Skip to content

[3/n] [ls-apis] track sled-agent-rack-setup as an embedded component#10459

Merged
sunshowers merged 7 commits into
mainfrom
sunshowers/spr/3n-ls-apis-track-sled-agent-rack-setup-as-a-separate-subcomponent
May 29, 2026
Merged

[3/n] [ls-apis] track sled-agent-rack-setup as an embedded component#10459
sunshowers merged 7 commits into
mainfrom
sunshowers/spr/3n-ls-apis-track-sled-agent-rack-setup-as-a-separate-subcomponent

Conversation

@sunshowers
Copy link
Copy Markdown
Contributor

@sunshowers sunshowers commented May 18, 2026

RSS has very different semantics from the rest of Sled Agent, and the recent split to put it in another crate (#10340) gives us a change to handle this divergence in a principled manner.

Introduce two new notions to ls-apis:

  • embedded component: components that are part of a deployment unit but should be treated separately. Currently, RSS is the only embedded component.
  • lifecycle: when a server component's code runs -- "steady-state" by default, but "rack-init" for RSS

This separation also gives us a chance to do a lot of cleanup within the API manifest:

  • the not-deployed evaluation for RSS goes away, since that knowledge can be derived from the rack-init lifecycle
  • there's no need for RSS to declare IDU-only edges, since it doesn't participate in the upgrade DAG

Depends on:

Created using spr 1.3.6-beta.1

[skip ci]
Created using spr 1.3.6-beta.1
# upgrade DAG, so that sled-agent-rack-setup doesn't impose any ordering
# constraints on the upgrade DAG.
subcomponents = [
{ name = "sled-agent-rack-setup", inside = "omicron-sled-agent", lifecycle = "rack-init" },
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking for feedback on the inside thing. I tried autodiscovering the inside by looking at the dependency graph but honestly that was a complete mess that was hard to understand both in the code and in this manifest. I feel like having an explicit inside makes both layers easier to understand.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this tbh. Having it explicitly set helped me understand what was going on. Coming to this PR with very little knowledge of how ls-apis worked, I can say that not having it would have made the functionality much harder to grasp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, making it explicit is much more approachable. ls-apis is necessarily quite complex and it's good to try and keep the complexity budget down.

Created using spr 1.3.6-beta.1

[skip ci]
Created using spr 1.3.6-beta.1
Copy link
Copy Markdown
Contributor

@karencfv karencfv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't have time to finish reviewing, but wanted to give you my initial questions. A lot of it is just me wanting to understand the ls-apis tool 😄

Comment thread dev-tools/ls-apis/README.adoc Outdated
Comment on lines +170 to +174
==== Subcomponents

Sometimes one binary contains a body of code whose API dependencies are best tracked separately from the rest of it. The motivating case is the Rack Setup Service (RSS): it lives in the `sled-agent-rack-setup` library crate, which is linked into the `omicron-sled-agent` binary, but it runs only once (at rack initialization) and has different semantics from the rest of the Sled Agent.

A _subcomponent_ is such a library, tracked as an API consumer in its own right:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I think of the concept of a "subcomponent", what comes to mind is a piece of a unit, sort of what packages does. I don't really think of a piece of a unit that should be treated differently. This terminology caused a bit of confusion for me and made it harder to understand what was going on here. At first glance it gave me the impression you should list all API dependencies or something like that.

Perhaps we could use another term? Something like tracked_subcomponentswhere it's clear that they're something that should be tracked separately? Or perhaps auxiliary_subcomponents?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see what you mean. What about "library component" to indicate that this is a library that should be reasoned about separately? Or is that too general for you?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, it still feels a little vague? What about independent_component?

Copy link
Copy Markdown
Contributor Author

@sunshowers sunshowers May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm my issue with that is that "an independent component that lives inside another component" is a bit weird. What about "embedded component" if "subcomponent" still doesn't work for you?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that's fair. I like embedded component! It's much more clear about its intent. Thanks for humouring me here 😄

Comment thread dev-tools/ls-apis/README.adoc Outdated
Comment thread dev-tools/ls-apis/README.adoc Outdated
]
```

`inside` names the top-level package (in the same deployment unit) that links the subcomponent. When ls-apis walks that package's dependencies, the subcomponent's own crate is skipped, so any API dependency reachable only through the subcomponent is attributed to the subcomponent instead of to the containing binary. (A dependency the binary also reaches by another path is attributed to both.) The subcomponent is then walked separately, as its own consumer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional nit. What about parent or parent-package instead of inside?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it's not necessarily a direct parent, so it would have to be called ancestor. I guess that might work, though I felt that ancestor is a little overloaded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, I can't think of a better name either lol. Whichever one is good

# upgrade DAG, so that sled-agent-rack-setup doesn't impose any ordering
# constraints on the upgrade DAG.
subcomponents = [
{ name = "sled-agent-rack-setup", inside = "omicron-sled-agent", lifecycle = "rack-init" },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this tbh. Having it explicitly set helped me understand what was going on. Coming to this PR with very little knowledge of how ls-apis worked, I can say that not having it would have made the functionality much harder to grasp.

Comment thread dev-tools/ls-apis/api-manifest.toml Outdated
Comment on lines +238 to +249
for pkg in &raw_unit.packages {
component_names.push(pkg.clone());
register_server_component(
&mut server_components,
ServerComponent {
name: pkg.clone(),
deployment_unit: raw_unit.name.clone(),
lifecycle: Lifecycle::SteadyState,
kind: ServerComponentKind::TopLevel,
},
)?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible in the future to have a package in a Lifecycle::RackInit lifecycle? If so, what do you envision happening?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly possible in principle, yeah, though I'm a little doubtful in practice. (I could be wrong, though!)

At that point we'd update the tooling to let you specify that.

sunshowers added a commit that referenced this pull request May 28, 2026
#10458)

We're going to return more data from this function (the set of omitted
nodes that were seen) in an upcoming PR.

I tried out a few different approaches:

* two callbacks
* a trait
* a hybrid approach where we keep the current callback and return the
additional data as a value

Based on the prototyping I feel like this ends up being the cleanest
approach.

There are no functional changes in this PR.

Best reviewed in conjunction with its dependent PR:

* #10459
Comment thread dev-tools/ls-apis/README.adoc Outdated
Comment on lines +170 to +174
==== Subcomponents

Sometimes one binary contains a body of code whose API dependencies are best tracked separately from the rest of it. The motivating case is the Rack Setup Service (RSS): it lives in the `sled-agent-rack-setup` library crate, which is linked into the `omicron-sled-agent` binary, but it runs only once (at rack initialization) and has different semantics from the rest of the Sled Agent.

A _subcomponent_ is such a library, tracked as an API consumer in its own right:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, it still feels a little vague? What about independent_component?

Comment thread dev-tools/ls-apis/README.adoc Outdated
]
```

`inside` names the top-level package (in the same deployment unit) that links the subcomponent. When ls-apis walks that package's dependencies, the subcomponent's own crate is skipped, so any API dependency reachable only through the subcomponent is attributed to the subcomponent instead of to the containing binary. (A dependency the binary also reaches by another path is attributed to both.) The subcomponent is then walked separately, as its own consumer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, I can't think of a better name either lol. Whichever one is good

Comment thread dev-tools/ls-apis/src/system_apis.rs Outdated
Comment on lines +188 to +196
"subcomponent {:?} (in deployment unit \
{:?}) is not a package in workspace {:?} \
(the workspace of its `inside` package \
{:?}); check for a typo in `name`, or that \
the package actually exists",
component.name(),
component.deployment_unit(),
workspace.name(),
inside,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little confused by this message. How about something like

anyhow!(
    "subcomponent {:?} not found in its expected workspace\n  \
     expected workspace: {:?} (the workspace of its `inside` package {:?})\n  \
     deployment unit: {:?}\n  \
     hint: check `name` for typos, or that the package exists in that workspace",
    component.name(),
    workspace.name(),
    inside,
    component.deployment_unit(),
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks.

Comment on lines +270 to +283
bail!(
"subcomponent {:?} (in deployment unit \
{:?}) is declared with `inside = {:?}`, \
but {:?} has no normal or build Cargo \
dependency on it; remove the stale \
`subcomponents` entry, correct its \
`inside` field, or restore the dependency \
if it was removed or made dev-only",
sub.name(),
sub.deployment_unit(),
component_name,
component_name,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

bail!(
    "subcomponent {:?} is declared `inside` a package that doesn't depend on it\n  \
     deployment unit: {:?}\n  \
     `inside` package: {:?} (no normal or build Cargo dependency on this subcomponent)\n  \
     hint: remove the stale `subcomponents` entry, fix its `inside` field, \
     or restore the dependency if it was removed or made dev-only",
    sub.name(),
    sub.deployment_unit(),
    component_name,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, thanks. I would like to switch this to an error collector pattern at some point (so collect all errors, don't just bail on the first one).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooh yeah, that'd be nice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +755 to +757
if !self.component_in_upgrade_dag(server_pkg) {
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not be the first condition? If the component is not the upgrade DAG at all why would we check how the API is versioned first?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're both independent determinations. For a particular (server, client) pair, in order for us to treat it as part of the upgrade DAG, both of the following must be true:

  1. The server component is part of the steady-state system.
  2. The API corresponding to the client is server-side versioned.

They can be done in either order -- it's arbitrary, and I don't have a strong preference either way. Do you prefer putting 1 before 2 in the code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Nah, either way is fine.

Comment on lines +844 to +846
if !self.component_in_upgrade_dag(server_component) {
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

@sunshowers sunshowers changed the base branch from sunshowers/spr/main.3n-ls-apis-track-sled-agent-rack-setup-as-a-separate-subcomponent to main May 28, 2026 19:18
Created using spr 1.3.6-beta.1
Created using spr 1.3.6-beta.1
Copy link
Copy Markdown
Contributor

@karencfv karencfv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it needs a cargo fmt but otherwise looks great thanks!

Created using spr 1.3.6-beta.1
@sunshowers sunshowers changed the title [3/n] [ls-apis] track sled-agent-rack-setup as a separate subcomponent [3/n] [ls-apis] track sled-agent-rack-setup as an embedded component May 28, 2026
@sunshowers sunshowers enabled auto-merge (squash) May 28, 2026 23:28
@sunshowers sunshowers merged commit acbe5e6 into main May 29, 2026
18 checks passed
@sunshowers sunshowers deleted the sunshowers/spr/3n-ls-apis-track-sled-agent-rack-setup-as-a-separate-subcomponent branch May 29, 2026 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants