Skip to content

Commit 9e8eb5a

Browse files
LeStarchthomas-bc
andauthored
Add docs on Selecting Component, Port, and Command Kinds (#5108)
* Adding guide for selecting components * Cross-linking * Grammer and spelling * Update docs/user-manual/framework/component-and-port-selection.md Co-authored-by: Thomas Boyer-Chammard <49786685+thomas-bc@users.noreply.github.com> * Fix review recomendations * Fix review II --------- Co-authored-by: Thomas Boyer-Chammard <49786685+thomas-bc@users.noreply.github.com>
1 parent d228151 commit 9e8eb5a

3 files changed

Lines changed: 283 additions & 9 deletions

File tree

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# Selecting Component, Port, and Command Kinds
2+
3+
This document will describe how to select the kind of component, port, and command to use when developing within the F Prime framework. We will focus on the component kinds (passive, queued, and active) and the critical port kinds (sync, async). We will begin by discussing the types of work performed by F Prime systems, then use that model to guide port and component kind selection.
4+
5+
This guide assumes you have a basic understanding of the different component and port kinds. If you are unfamiliar with these concepts, please see [Core Constructs: Ports, Components, and Topologies](../overview/03-port-comp-top.md) for an introduction to these concepts.
6+
7+
> [!NOTE]
8+
> This document does not discuss output ports as the kind of port is determined on the input side of the connection.
9+
10+
> [!IMPORTANT]
11+
> Commands are implemented in F Prime via ports. Thus they have the same kinds (`async`, `sync`, and `guarded`) and for the purposes of this document, they follow the same decision process as ports.
12+
13+
**Table of Contents**
14+
- [Types of Work in F Prime Systems](#types-of-work-in-f-prime-systems)
15+
- [Component Selection for Cyclic Work](#component-selection-for-cyclic-work)
16+
- [Passive Components for Cyclic Work](#passive-components-for-cyclic-work)
17+
- [Queued Components for Cyclic Work](#queued-components-for-cyclic-work)
18+
- [Event-Driven Work](#event-driven-work)
19+
- [Background Work](#background-work)
20+
- [Hybrid Patterns](#hybrid-patterns)
21+
- [Cyclic Notification Pattern](#cyclic-notification-pattern)
22+
- [Active Anchor Pattern](#active-anchor-pattern)
23+
- [Passive Converter Pattern](#passive-converter-pattern)
24+
- [Conclusion](#conclusion)
25+
26+
## Types of Work in F Prime Systems
27+
28+
Work in an F Prime system breaks down into three categories roughly driven by the timing requirements of the work. These are:
29+
30+
1. Cyclic Work: Cyclic work is how F Prime addresses hard deadlines. This work is performed on a repeating schedule driven by a [Rate Group](../design-patterns/rate-group.md). e.g. send a control update every 10ms.
31+
2. Event-Driven Work: Event-driven work is how F Prime addresses timely work lacking hard deadlines. e.g. dispatch commands reasonably quickly.
32+
3. Background Work: Background work is how F Prime addresses work without timing requirements. e.g. log telemetry to disk.
33+
34+
Two other relevant terms are synchronous and asynchronous invocations (i.e. how a port executes). Synchronous invocations happen immediately and block the caller until completion just like a typical function call. Asynchronous invocations are queued up until some point in the future when the receiver processes them. Synchronous ports are invoked synchronously and asynchronous ports are invoked asynchronously and backed by a queue in the receiver component.
35+
36+
> [!IMPORTANT]
37+
> Cyclic work is almost always performed via synchronous invocations while Event-Driven and Background work is typically performed via asynchronous invocations.
38+
39+
Understanding the type of work your component will perform is the first step to selecting the appropriate component and port kinds. We will discuss component selection for each type of work.
40+
41+
## Component Selection for Cyclic Work
42+
43+
When performing cyclic work, it is crucial to know whether all work in the cycle will complete before the cycle repeats, because this 'slip' indicates a failure to meet the cycle’s hard deadline. For example, a 10 Hz control update must happen every 100 ms. If one iteration takes longer than 100 ms, the cycle has slipped and system control has been compromised because it did not keep up with the expected control rate. For this reason, cyclic work is almost always performed via synchronous invocations and therefore uses a sync port. Asynchronous invocations are not used because this work would be run outside the rate group cycling context and thus slips and failure to reach deadlines would be hidden in another thread.
44+
45+
> [!CAUTION]
46+
> Remember, `guarded` ports are also synchronous with an internal mutex to protect data. These are not as common in cyclic work and a full discussion of `guarded` ports is outside the scope of this document.
47+
48+
Since the primary mode of invocation is synchronous when doing cyclic work, we will choose a component kind that primarily handles synchronous invocations, i.e., a `passive` or `queued` component.
49+
50+
### Passive Components for Cyclic Work
51+
52+
Passive components are the natural choice for cyclic work as we intend them to execute in the context of the invoking rate group. A good starting model for cyclic work is to have a passive component with a `sync` port of type `Svc.Sched` that performs the repeating work for that component each cycle.
53+
54+
You may add output ports as needed for the component to interact with other components, but the primary work of the component will be performed in the `Sched` handler. This is a simple and common model for cyclic work.
55+
56+
A timing diagram of this model is shown below.
57+
```mermaid
58+
sequenceDiagram
59+
participant R as Rate Group
60+
participant C1 as Passive Component
61+
participant C2 as Optional Component
62+
63+
loop Every Cycle (e.g. every 100ms)
64+
R->>+C1: Cyclic Invocation (Svc.Sched)
65+
C1->>C1: Perform work
66+
C1->>+C2: (Optional) Interact with other components
67+
C2->>C2: Perform work
68+
C2-->C1:
69+
deactivate C2
70+
C1-->>R:
71+
deactivate C1
72+
end
73+
```
74+
**Figure 1**: a rate group driven passive component that may call another component as part of its cyclic execution.
75+
76+
> [!CAUTION]
77+
> This simple `passive` pattern breaks down when the cyclic component needs to accept some Event-Driven work (e.g. it processes some commands). This use case is described in the next section.
78+
79+
### Queued Components for Cyclic Work
80+
81+
When a component performing cyclic work also needs to accept some Event-Driven work, we require a queue to handle the asynchronous invocations, but adding a queue processing thread may disrupt the critical synchronous invocations of the core cyclic work of the component. For this exact reason, we use a `queued` component. A `queued` component allows asynchronous events to be accepted while keeping a synchronous core. In this model, the component dispatches the queue as part of the primary synchronous invocation (i.e. `Svc.Sched` handler) thereby moving the asynchronous work into the cycle.
82+
83+
We do not typically used `sync` commands in this context because a synchronous command would block the command dispatch while executing, and also requires mutex protection to access shared data. These mutex accesses can disrupt the critical cyclic work by occurring at anytime whereas the queue is processed at a specific point in the cycle.
84+
85+
Adding a thread to handle the asynchronous work would either mean the cyclic work is moved to this thread (again hiding slips and failures to meet deadlines) or the component thread and rate group thread would need concurrency protection that causes disruption.
86+
87+
> [!IMPORTANT]
88+
> In most situations, you want a singular thread to execution to do all the work of the component. If a rate group is driving the component, use a queue to pull asynchronous work into that thread. If you have a Event-Driven components (see below) then all interaction should be `async` as to be performed on the component's thread.
89+
90+
91+
```mermaid
92+
sequenceDiagram
93+
participant R as Rate Group
94+
participant C1 as Queued Component
95+
participant C2 as Event Source
96+
97+
C2-)C1: Asynchronous Invocation (command or port)
98+
C2-)C1: Asynchronous Invocation ...
99+
100+
loop Every Cycle (e.g. every 100ms)
101+
R->>+C1: Cyclic Invocation (Svc.Sched)
102+
C1->>C1: Dispatch queue
103+
C1->>C1: Perform work
104+
C1-->>R:
105+
deactivate C1
106+
end
107+
```
108+
**Figure 2**: a rate group driven queued component that dispatches asynchronous events as part of its cyclic execution.
109+
110+
> [!IMPORTANT]
111+
> In this model, it is **imperative** that you dispatch the queue in some synchronous implementation (i.e. the `Svc.Sched` handler); otherwise, the queue will fill but events will never process.
112+
113+
## Event-Driven Work
114+
115+
Since Event-Driven work is typically high-priority but without strict hard deadlines, this work is typically done via asynchronous invocations and uses the `async` port kind. Since the component lacks another context to run in, we use an `active` component to dispatch the asynchronous work.
116+
117+
This model is constructed by having any number of `async` ports and commands attached to an `active` component. The thread scheduler handles the rest.
118+
119+
Synchronous Event-Drive work is typically avoided because that work would block the source of the event while it is processed. This causes unnecessary coupling between components leading to more complicated system design. Thus Event-Drive work is typically performed asynchronously. We use an `active` component because it has its own thread to processes asynchronous work.
120+
121+
> [!IMPORTANT]
122+
> Here again we see uniformity in our invocations. In an `active` Event-Driven component, all ports and commands are typically `async` to that they all process on the same thread. Other patterns require concurrency protection, which is more complicated and can couple components' executions together.
123+
124+
```mermaid
125+
sequenceDiagram
126+
participant E as Event Source
127+
participant C1 as Active Component
128+
129+
130+
E-)C1: Asynchronous Invocation (command or port)
131+
E-)C1: Asynchronous Invocation ...
132+
133+
loop Forever (Thread Lifecycle)
134+
C1->>+C1: Wait for event
135+
C1->>C1: Dispatch event
136+
deactivate C1
137+
C1-)E: (Optional) Event source callback
138+
end
139+
```
140+
**Figure 3**: an active component that dispatches asynchronous events as part of its thread lifecycle.
141+
142+
## Background Work
143+
144+
In F Prime, background work is typically performed via asynchronous invocations and thus uses the `async` port kind. Since the component lacks another context to run in, we use an `active` component to dispatch the asynchronous work via a thread.
145+
146+
This model is identical to the Event-Driven work model running at a lower thread priority, thus requiring more careful queue management. The event source for background work should emit only a small number of events until the background work is indicated as complete ([see the port callback pattern](../design-patterns/common-port-patterns.md#callback-ports) for more details on how to indicate that background work is complete) in order to prevent queue overflows.
147+
148+
149+
```mermaid
150+
sequenceDiagram
151+
participant E as Event Source
152+
participant C1 as Active Component
153+
154+
155+
E-)C1: Asynchronous Invocation (start command or port)
156+
157+
loop Forever (Thread Lifecycle)
158+
C1->>+C1: Wait for event
159+
C1->>C1: Dispatch long-running event
160+
deactivate C1
161+
C1-)E: Event complete callback
162+
end
163+
E-)C1: Asynchronous Invocation (start command or port)
164+
```
165+
**Figure 4**: an active component that dispatches background events as part of its thread lifecycle. Here a callback indicates the background work is complete and another event can be handled.
166+
167+
> [!IMPORTANT]
168+
> Components performing background work should be engineered to accept only a small number of events at a time to prevent queue overflows. An example is the [Manager/Worker pattern](../design-patterns/manager-worker.md).
169+
170+
## Hybrid Patterns
171+
172+
Sometimes component design does not neatly fit into the above categories. This section will elaborate on some common "hybrid" patterns that combine the above models.
173+
174+
> [!CAUTION]
175+
> This section is intended to give developers a deeper understanding of real-world designs. You should prefer the simpler models above wherever possible.
176+
177+
### Cyclic Notification Pattern
178+
179+
We discussed what happens when a cyclic component needs to accept the occasional Event-Driven work, but what if a primarily Event-Driven component needs to perform the occasional cyclic work? For example, a component needs to emit telemetry at a regular interval that is not a strict deadline.
180+
181+
In this case, we can use the "Cyclic Notification Pattern" where an `active` component performs primarily Event-Driven work but also has an `async` port of type `Svc.Sched` that converts the cyclic invocation into a queued event that is processed roughly at the cycle interval.
182+
183+
```mermaid
184+
sequenceDiagram
185+
participant R as Rate Group
186+
participant C1 as Active Component
187+
188+
loop Every Cycle (e.g. every 1S)
189+
R-)C1: Cyclic Invocation (Svc.Sched)
190+
end
191+
192+
loop Forever (Thread Lifecycle)
193+
C1->>+C1: Wait for event
194+
C1->>C1: Dispatch event
195+
deactivate C1
196+
end
197+
```
198+
**Figure 5**: an active component that dispatches events as part of its thread lifecycle. Some events are generated by a cyclic invocation via a `Svc.Sched` port.
199+
200+
### Active Anchor Pattern
201+
202+
Sometimes the work done by an Event-Driven component is easier to decompose into multiple components. In this case, there is typically an Event-Driven active component that orchestrates a set of passive helper components as part of its handling of events.
203+
204+
```mermaid
205+
sequenceDiagram
206+
participant E as Event Source
207+
participant C1 as Active Component
208+
participant H1 as Passive Helper 1
209+
participant H2 as Passive Helper 2
210+
211+
E-)C1: Asynchronous Invocation (command or port)
212+
E-)C1: Asynchronous Invocation ...
213+
214+
loop Forever (Thread Lifecycle)
215+
C1->>+C1: Wait for event
216+
C1->>C1: Dispatch event
217+
C1->>+H1: Synchronous Invocation
218+
H1->>H1: Perform some work
219+
H1-->>C1:
220+
deactivate H1
221+
C1->>+H2: Synchronous Invocation
222+
H2->>H2: Perform some work
223+
H2-->>C1:
224+
deactivate H2
225+
deactivate C1
226+
end
227+
```
228+
**Figure 6**: an active component that dispatches events as part of its thread lifecycle using a series of passive helper components for a more nuanced decomposition.
229+
230+
### Passive Adapter Pattern
231+
232+
Sometimes you just need a component that does some menial conversion or other work as part of what is logically another port call. For example, you need to connect two components with incompatible port types and need to reconcile those types. In this case, you can use a passive component as an adapter that is called synchronously as part of the primary port call.
233+
234+
```mermaid
235+
sequenceDiagram
236+
participant S as Source Component
237+
participant C as Adapter Component
238+
participant D as Destination Component
239+
240+
S->>+C: Synchronous Invocation
241+
C->>C: Perform conversion
242+
C->>D: Synchronous/Asynchronous Invocation
243+
deactivate C
244+
```
245+
**Figure 7**: a passive adapter component that performs a conversion inline with a port call.
246+
247+
## Conclusion
248+
249+
This document covers the basics of component and port kind selection in F Prime. It should give you a starting point for making informed decisions when developing F Prime components. However, there are always times where a real design may depart from these models. The important thing is to understand why and be able to justify it.
250+
251+
252+
The following table maps the key patterns in this document to concrete examples in the F Prime codebase.
253+
254+
| Pattern | Typical Shape | Example Components/Subtopologies |
255+
| --- | --- | --- |
256+
| Passive Components for Cyclic Work | `passive` component with `sync` `Svc.Sched` input | N/A* |
257+
| Queued Components for Cyclic Work | `queued` component with `sync` schedule input and async/event inputs | `Svc.Health` |
258+
| Event-Driven Work | `active` component with primarily `async` command/port inputs | `Svc.EventManager` |
259+
| Background Work | `active` component handling non-deadline work asynchronously | `Svc.FileWorker` |
260+
| Cyclic Notification Pattern | `active` component with an `async` `Svc.Sched` input used as periodic notification | `Svc.TlmPacketizer` |
261+
| Active Anchor Pattern | One active "anchor" plus passive helper components | `ComCcsds.FramingSubtopology` (`Svc.ComQueue` with passive helpers like `Svc.Ccsds.TmFramer`) |
262+
| Passive Adapter Pattern | `passive` adapter/converter called inline with existing flow | `Drv.ByteStreamBufferAdapter` |
263+
264+
> [!NOTE]
265+
> * The core F Prime codebase does not deal with any hard deadlines and thus does not have a canonical example of a purely cyclic component.

docs/user-manual/overview/02-fprime-architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Slide decks regarding the F´ Software Architecture, as well as general Flight S
44

55
These materials are updated yearly and are used during our [Events](https://fprime.jpl.nasa.gov/events/).
66

7+
For an explanation of the basic F Prime concepts see [Core Constructs: Ports, Components, and Topologies](./03-port-comp-top.md).

0 commit comments

Comments
 (0)