Component guidelines¶
These conventions apply to every Agent component. See the component framework overview for the design rationale and the creation tutorial for a worked example.
Ownership and interfaces¶
- Each component must be owned by one team responsible for its support and maintenance.
- Hide implementation details behind a small public interface named
Component, and keep concrete implementations private. - Make components reusable across binaries and external consumers. Implementation changes that preserve the interface and its behavior should not require QA of consumers.
- Avoid exposing third-party types in public interfaces unless they are intrinsic to the component's purpose. Docker types can make sense in a Docker component, but exposing them through a generic container component forces every consumer to depend on Docker.
- Test each component's behavior through its interface.
File hierarchy¶
All components are located in the comp folder at the top of the Agent repo.
The file hierarchy is as follows:
comp /
<bundle name> / <-- Optional
<comp name> /
def / <-- The folder containing the component interface and ALL its public types.
impl / <-- The only or primary implementation of the component.
impl-<alternate> / <-- An alternate implementation.
impl-none / <-- Optional. A noop implementation.
fx / <-- All fx related logic for the primary implementation, if any.
fx-<alternate> / <-- All fx related logic for a specific implementation.
mock / <-- The mock implementation of the component to ease testing.
To note:
- If your component has only one implementation, it should live in the
implfolder. - If your component has several implementations instead of a single implementation, you have multiple
impl-<version>folders instead of animplfolder. For example, a compression component can haveimpl-zstdandimpl-zipfolders instead of animplfolder. - If your component needs to offer a dummy/empty version, it should live in the
impl-nonefolder.
Package and constructor conventions¶
- Keep the interface and all public types in
def. Consumers should not need to import implementation packages. - Name implementation packages
<COMPONENT_NAME>impl, or<IMPL_NAME>implfor an alternate implementation, and expose aNewComponentconstructor. - Declare constructor dependencies in a public
Requiresstruct and outputs in a publicProvidesstruct. Keep their fields public so the Fx wrapper can use them. - Return
Providesfrom an infallible constructor, or(Provides, error)when construction can fail. - Confine Fx imports and references to the
fxpackages. Eachfx.gomust exposefunc Module() fxutil.Moduleand wrap the corresponding implementation. - Supply a
fx-nonewrapper when consumers need an absent optional implementation. See optional dependencies.
Mocks¶
Components must provide a mock implementation unless their public interface has no methods. A mock must implement the interface in def and may expose additional testing helpers. Mock constructors must accept a *testing.T parameter.
Go modules¶
Go modules are optional. To make a component available outside this repository, create separate modules for its def package, the implementations being exported, and its mock package. Never put a Go module at the component root or in an Fx wrapper package.
See adding nested modules for the procedure.
Concurrency and lifecycle¶
Components must be thread safe, tested, and documented. Public methods must be usable as soon as construction completes, although they may do nothing or drop data before the Agent finishes initialization. Document these behaviors and use lifecycle hooks for startup and shutdown work.
Documentation¶
The documentation (both package-level and method-level) should include everything a user of the component needs to know. In particular, the documentation must address any assumptions that might lead to panic if violated by the user.
Detailed documentation of how to avoid bugs in using a component is an indicator of excessive complexity and should be treated as a bug. Simplifying the usage will improve the robustness of the Agent.
Documentation should include:
- Precise information on when each method may be called. Can methods be called concurrently?
-
Precise information about data ownership of passed values and returned values. Users can assume that any mutable value returned by a component will not be modified by the user or the component after it is returned. Similarly, any mutable value passed to a component will not be later modified, whether by the component or the caller. Any deviation from these defaults should be documented.
Note
It can be surprisingly hard to avoid mutating data -- for example,
append(..)surprisingly mutates its first argument. It is also hard to detect these bugs, as they are often intermittent, cause silent data corruption, or introduce rare data races. Where performance is not an issue, prefer to copy mutable input and outputs to avoid any potential bugs. -
Precise information about goroutines and blocking. Users can assume that methods do not block indefinitely, so blocking methods should be documented as such. Methods that invoke callbacks should be clear about how the callback is invoked, and what it might do. For example, document whether the callback can block, and whether it might be called concurrently with other code.
- Precise information about channels. Is the channel buffered? What happens if the channel is not read from quickly enough, or if reading stops? Can the channel be closed by the sender, and if so, what does that mean?