{% set rfcid = “RFC-0121” %} {% include “docs/contribute/governance/rfcs/_common/_rfc_header.md” %}
This document captures design deliberations, principles, and decisions made regarding component events: concepts, manifest syntax and FIDL protocols.
Many of the decisions described below were made and implemented in early 2020. Some other concepts are proposed which tackle shortcomings and complexity in earlier decisions. Many features are proposed to be allowlisted to prevent their usage outside of existing use cases while we work to migrate them to better mechanisms.
The scope of this doc covers the following Component Framework APIs:
fuchsia.sys2/events.fidl
: the current API surface where Component Events FIDL APIs live. The goal is that all event APIs upgrade to fuchsia.component/events.fidl
and make them available in the SDK.Component manager handles lifecycle of components internally and doesn't expose that information to components. Some privileged clients (for example: diagnostics, tests, component supervisor, debuggers) need a bit more insight into lifecycle of components to perform their work.
Component events were introduced as a solution for exposing such information to these privileged components. All these events are dispatched by component manager when a lifecycle transition of a component instance happens. The capability routing API was left open in some places that are called out in this document for further design that allow components to expose and offer custom events themselves.
This section outlines the current design of Component Events together with API revisions, highlights where the current design is not intended as the long-term design and calls out future features that could be designed and implemented.
Component events are modeled as “event stream” capabilities. Each event stream capability refers to a single component or a sub-tree of components in the topology. At the time of writing this document, events are treated as individual event capabilities (not event streams). Component manager emits events on behalf of the components when a lifecycle transition occurs.
Like any capability, event streams can be routed. When an event stream is:
Exposed/Offered from framework: the event refers to the component that is exposing or offering the event.
Used from parent/child: a component can listen for events that were routed to it from a child or from the parent. The parent of a component can directly use the lifecycle event streams from a child without the child explicitly exposing the event from framework.
Event streams can be used/offered by the root realm from “parent”. You might be wondering, what is the parent of the root realm if it's supposed to be the root of the topology? In component manager terminology, capabilities offered by component manager to the root realm are said to come from “above root”.
Event streams offered from “above root” to the root realm are scoped to the whole component instance topology. These event streams can be downscoped to refer to a subtree of the topology when they are routed. This can be seen similar to how directory capabilities can be routed as a whole or as subdirectories (subdir
key in the routing declarations of directories). The only way to listen for events scoped to a whole realm is to have a downscoped event stream capability originating from the root available. Events covering a whole realm tree are privileged/sensitive and break encapsulation boundaries, therefore, we explicitly route them from above root providing access control through static routing.
Consider the following example:
// 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", } ] }
In this example:
foo
can get started
events for bar
given that test-12345
routes it to it.
archivist
can get started
events for all components under tests
given that it was offered started
from test_manager
that got it from core
that got it from root
that got it from above root
and was downscoped on the way.
test-12345
, the test root, can get started events about all components under it, for the same reasons that archivist
can. However, unlike the archivist (which can get all events from tests
), it can only get events about the test, given that tests
downscoped the event for test-12345
.
This example shows one of the core use cases of events at the moment. The archivist is able to observe what happens inside each test in an isolated manner. Also, each test can get events about all components under the test or about a specific one.
Event streams of the same type can be merged as a single stream. For example, in the example above, #test-12345
could offer the stopped
from foo
and bar
to some other component as a single capability. That component would then get the stopped
event for both foo
and bar
.
// core.cml offer: [ { event_stream: "stopped", from: [ "#netstack", "#supervisor" ], to: "#someone", } ]
The ability to expose/offer an event from self is not allowed for now. However, there's room for growth here to allow events to expose/offer custom events they dispatch themselves.
At the time of writing this document, events can be ingested in an asynchronous or a synchronous way. The intent is to have only async events and deprecate sync events entirely.
Consuming an event synchronously allows the subscriber to block component manager while it handles the event and then resume. This was introduced for testing initially and also with debuggers in mind.
Since then, we have learned that tests using sync events can be written using async events in general. Consequently, there‘s been work towards eliminating sync events entirely but a few uses remain in component manager internal tests. It’s believed that sync events will be useful in a debugger (such as step or zxdb), but at that point we'll invent a solution that accurately fits the needs of a debugger.
The proposal is to allowlist tests that use sync events and work towards eliminating sync events entirely.
At the time of writing this document, we have two classes of events:
We have the following lifecycle event types:
Discovered
: This is the first stage in the lifecycle of components. Dispatched for dynamic children when they're created, for static children when their parent is resolved, and for the root when the component manager starts.Resolved
: An instance's declaration was resolved successfully for the first time.Started
: This instance has started, according to component manager. However, if this is an executable component, the runner has further work to do to launch the component. The component is starting to run, but may not have started executing code.Stopped
: An instance was stopped successfully. This event must occur before Destroyed.Destroyed
: Destruction of an instance has begun. The instance has been stopped by this point. The instance still exists in the parent's realm but will soon be removed.Purged
: An instance was destroyed successfully. The instance is stopped and no longer exists in the parent's realm.And the following deprecated event types:
Running
: This event is synthesized by component manager for all components that are already running at the moment of subscription. This event is derived from started
, but for components that started (and didn't stop) before the listener subscribed for events. Eventually, the desire is to have a Component Framework Query API to understand what is running. Therefore, the plan is to allowlist this event to its only client, the Archivist, and at a future time use the new API and remove running
.
Capability Routed
: This event was introduced for usage in tests. It has recently been removed entirely from tests using it and we are in the process of completely removing it.
Capability Requested
: This event was introduced as a temporary solution to provide component attribution to fuchsia.logger/LogSink
connections. Since then it's also been used to provide attribution to fuchsia.debugdata/Publisher
connections. This was never meant to be a long term solution. Since the events system was for privileged components only, building this feature with Component Events was a low-commitment approach. It was understood that, if the use cases grew, the time will come to invent a more standardized solution. The current plan is to work to remove this event entirely and in the meantime allowlist it to its only clients: Archivist, debug data and test manager (for debug data).
Directory Ready
: This event was introduced as a solution to provide the out/diagnostics
directory exposed by components to the Archivist for inspect data aggregation. The Diagnostics team has plans to design VMO backed logs. Given that logs need to be available before the component starts serving the diagnostics directory, this approach becomes obsolete. A new solution for providing the inspect and logs VMO to the Archivist will be designed that guarantees that logs are available even before the component starts its async loop. The proposal for now is to allowlist this event to the only user it has (the Archivist) and work towards removing it entirely.
{ use: [ { event_stream: [ "running", "started", "stopped", ], from: "parent", mode: "async", path: "/my_stream" }, ] }
The use declaration contains:
event_stream
: a single event name or a list of event names.from
: the source of the capability. Allowed values: parent or a child reference. Using an event from framework or self is not allowed.path
: The path where the event stream will be served. This is optional. When given the incoming namespace of the component will contain a service file with the given name for a fuchsia.component.EventStream
that component manager serves (see Use). When not provided, the component can use EventSource
to start consuming events at a specific point.scope
: when an event is used from framework, the scope is required to specify the child (or array of children) which the event will be about. When the event is used from parent, the scope
can be used to refer downscope the event to a certain child scope, otherwise the event will carry the scope coming from the parent.mode
: defaults to async
. As mentioned earlier, the only event mode will be async. Therefore only allowlisted tests will be permitted to use mode “sync” until modes are completely eliminated. This field will eventually go away entirely.filter
: this is currently used only for DiagnosticsReady
and for CapabilityRequested
. As mentioned earlier, these events are being removed entirely so no details are provided about filters as they won't be relevant to a non-diagnostics developer.It's also possible to use an event coming from different sources. In the following example, a component will get a single event stream with started
and stopped
events scoped as it was offered by its parent, as well as a started
when its child #child
starts.
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" }, ] }
The offer declaration contains:
event_stream
: a single event name or a list of event names.scope
: when an event is offered from framework, the scope allows one to define the child (or an array of children) of which the event is about. If no scope is specified, the scope is self, meaning the event is about the component itself. When an event is offered from parent, the scope allows to downscope the event to a child scope.from
: one or multiple sources of the capability. When multiple sources are given the streams are considered to be merged. Allowed values: framework, parent or a child reference. Offering from self is not allowed.to
: the child reference to which the capability is offered.as
: the target name of the capability (rename). This can only be provided when a single event stream name is given.{ expose: [ { event_stream: "started", from: "framework", scope: [ "#child", "#other_child" ], as: "foo_started" }, ] }
The expose declaration contains:
event_stream
: a single event name or a list of event names.scope
: when an event is exposed from framework
, the scope is required and allows one to define the child (or array of children) which the event is about.from
: one more more sources of the capability. When multiple sources are given the streams are considered to be merged. Allowed values: parent or a child reference. Exposing an event from self is not allowed.as
: the target name of the capability (rename). This can only be provided when a single event stream name is given.EventSource
protocolEventSource
is the protocol that allows component instances to subscribe to event streams. It's a builtin capability any component can use from framework
to consume events that are explicitly routed to them.
Static event streams are a way of acquiring events through a protocol in the incoming /events
directory. Unlike runtime subscription through EventSource.Subscribe
, events can buffer and trigger a startup of the component when they arrive.
Components declare they use
an event stream at some path and component manager will create an entry in their incoming namespace at the given path which is a fuchsia.component.EventStream
served by component manager (see Use section).
Events are buffered internally until a GetNext
call is performed. The size of this buffer will be decided during implementation (it could be a large buffer or the maximum size of the next batch or something else) and will be clearly documented in the FIDL API and events documentation. If the number of buffered events exceeds the defined amount (due to a consumer being too slow) component manager will drop events and close the connection. The component can then reconnect when it‘s ready to receive events. This is a workaround for lack of flow control in Zircon. We want to prevent channel overflows which wouldn’t be possible if we were to invert who serves the EventStream
protocol (the component instead of component manager) and we don‘t want to use excessive amount of memory in component manager. A well-behaved client which consumes events at a steady rate won’t need to worry about missing events. Clients consuming events too slowly will be notified of dropped events. This will happen through an event in the stream from which they are consuming. A couple alternatives to perform this notification could be:
fuchsia.component.EventErrorPayload
: dropped
. This field would contain an EventType
and a number indicating the number of events of that type that were dropped.At the time of writing this document, the protocols for consuming events are defined in fuchsia.sys2/events.fidl
. This proposal introduces a few changes with the goal of upgrading the protocols and structures to fuchsia.component
and make them available in the SDK:
EventStream
instead of the componentThis implies changing EventSource#Subscribe
to take a request:
[Discoverable] protocol EventSource { Subscribe(vector<EventSubscription> events, request<EventStream> stream) -> () error fuchsia.component.Error; };
EventStream.GetNext
provides a batch of eventsSince component manager now serves the protocol and batches events, as discussed in a previous section, the new stream protocol returns a batch of events on GetNext
rather than a single event.
protocol EventStream { GetNext() -> vec<Event>; };
TakeStaticEventStream
is droppedThe method EventSource.TakeStaticEventStream
is removed in favor of placing the event stream directly in the incoming namespace as discussed in the previous section.
Event
and EventResult
At the time of writing this document, all these data types are tables. However, their data is very well defined and we don‘t expect to add more to it. To improve ergonomics by removing handling of optional fields, we’ll make them struct
s and non flexible unions:
struct Event { EventHeader header; EventResult event_result; }; union EventResult { 1: EventPayload payload; 2: EventError error; };
Most of this design is already implemented. A few bits remain that need to be implemented before we expose this API in the SDK. In particular:
directory ready
, running
and capability requested
.EventStream.TakeStaticEventStream
method.events
and we want to route event streams
which can be merged in routing.scope
keyword in event routing declarations to allow to have events from a subtree in the topology.offer
and use
.The event system builds on top of the existing internal hook system that component manager implements, therefore the performance implications of dispatching the events to other components interested in them are negligible. In fact, there are improvements in performance since now if a component is interested in only events from a single component and not a whole subtree, now it's possible to dispatch only events for that component instead of the whole subtree reducing the number of syscalls involved.
This RFC introduces improvements in ergonomics of events:
EventSource
protocol available as a builtin framework capability and they don't require routing it explicitly.event stream
instead of routing events
and consuming event streams
as today.Event streams
can be merged when routing, reducing the amount of routing statements developers need to write in CML.This change does not break compatibility. There‘ll be in-tree trivial refactors at the component manager level (and root, core, bootstrap realms) but not in client components. Additionally, there’ll be refactors in tests that use events today (all in-tree), since events will be about a single component now.
This proposal maintains security properties discussed in a security and privacy review in 2020. Previously the EventSource
protocol was explicitly routed since events were about a whole subtree. Now this protocol is a builtin capability and events are about specific components. Events about a whole subtree are explicitly routed and static verification can be performed to ensure non-privileged components aren't getting these events.
All event features and semantics must have integration testing in component manager.
Updates to documentation must be made in the Event Capabilities and Component Manifests pages.
EventSink
capability from a component to frameworkUnder this idea, a component would expose a protocol RealmEventSink
to framework. The framework would then open this capability and push events into it as they occur. This proposal lacks a way of defining from what scope the component will get the events and there‘s no clear path to restrict such a scope or be able to statically verify that a component is not getting events from a part of the topology from which it should have no visibility. Under the current design, it’s statically possible to express getting events about a specific set of components (for regular use cases) or a whole subtree in the topology (for diagnostics use cases in production and tests).
expose/offer
event from self
is left as an open area for future design work of custom events that a component might expose and dispatch itself.
Documentation about the current state of events can be found in the Event capabilities and in the Events FIDL definitions.