In omicron and other Oxide projects, we use UUIDs to identify many different
kinds of entities. Using a single Uuid type for all of them risks mixing up
all the different kinds of IDs.
To address that, we’re actively moving towards typed UUIDs with the newtype-uuid crate. The goal is that each kind of entity will have a marker type representing a UUID kind associated with it. Then, the type system will make it significantly harder to mix different kinds of entities.
omicron-uuid-kinds is a centralized registry for UUID kinds, one that can
be shared across Oxide repos. omicron-uuid-kinds supports no-std so the kinds
can be shared with embedded code as well.
Adding a new UUID kind
Start by adding a new element to the invocation of the impl_typed_uuid_kind! macro in src/. For example:
impl_typed_uuid_kind! {
// ...
Widget => "widget",
// ...
}This will:
Create a new
TypedUuidKindcalledWidgetKind. This kind will become the type parameter toTypedUuidand other related generic types.Create a type alias
type WidgetUuid = TypedUuid<WidgetKind>.
Then, start changing your UUID types over. It’s generally easiest to change the type of a UUID in a struct or enum field, and start pulling that thread.
If your UUID isn’t used in too many places, you can usually just change all users in one go. For an example, see the conversions of several UUID types in #5135.
If your UUID is widely used, you may need to break your change up across several commits. It’s easiest to carve out a section of your code to make changes in, and use the
GenericUuidconversions into and out of this code. For an example, see the ongoing conversion for sled UUIDs in #5404 and #5488.
For TypedUuid<T>, prefer to use the type aliases. For example, SledUuid rather than TypedUuid<SledKind>.
Some older code is still being ported over to type aliases; see #5511 for an example.
Other types that use the same kinds, like nexus_db_model::DbTypedUuid<T> and
typed_rng::TypedUuidRng<T>, don’t have aliases defined for them. That’s
because their frequency of use falls below the threshold at which the benefits
of type aliases outweigh the costs.
Some special cases:
If part of your change is at an OpenAPI schema boundary, then you generally also want to have clients use the same UUID types. The best way to do that currently is to use a
replacedirective, as in #5135. There is ongoing design work to make this work automatically, in typify#503 and elsewhere.For Nexus database storage,
nexus-db-modelhas aDbTypedUuidgeneric type which can be used.DbTypedUuidshould not be exposed outside ofnexus-db-model; instead, prefer to use the regularTypedUuidand only convert toDbTypedUuidwithin the lowest layers (i.e. using getters/setters and not making it a public field). This is because the regularTypedUuidhas much more infrastructure built around it.
Determinations
As part of this effort, we’ve made several decisions that could have gone a different way. This section documents those choices.
One big registry or several smaller ones?
Rather than having a single registry, an option is to have several smaller ones (perhaps one to two in each repo). The best answer to that is currently unclear, but putting them in the same crate for now has several advantages:
The macros updated in this PR know where uuid kinds will be found, so they let you specify just a bare name as the kind.
It’s simpler.
The disadvantage of this is that any change to this registry will cause all of omicron to be rebuilt. Hopefully, once most UUIDs are converted over, this crate won’t be touched too much.
This decision involves some level of commitment, because splitting up a registry will probably require several days of work. For example, macros may need to be updated to handle multiple sources, import paths would have to be changed across several repos, and so on.
Where should the registry live?
Once we’ve decided on a single registry, the next question is where this
registry should be stored. There was some debate in #oxide-control-plane, and
two options stood out:
In omicron itself.
The upside of this is that it is easy to add new UUID kinds within omicron, especially as we transition over existing untyped UUIDs.
The downside is that we also want to use these kinds in crucible and propolis — and pulling in
omicron-uuid-kindsas a Git dependency would typically cause two copies ofomicron-uuid-kindsto be floating around. That can result in a lot of pain, including confusing errors.
In a separate repo. While it eliminates the issue with duplicated dependencies, it does add a fair bit more friction. That’s because users updating omicron would now have to make two separate, coordinated PRs.
We’ve chosen 1 for now, because agility within omicron outweighs other
concerns. The downside is mitigated by using a [patch] directive; see the
workspace root’s Cargo.toml for more about this.
This is straightforward to change in the future. If we find that 1 is too much of a bother, we’ll easily be able to switch to 2 while keeping the name of the crate the same (minimizing churn).