Skip to content

Commit

Permalink
refac(console): use a newtype to separate ID types (#358)
Browse files Browse the repository at this point in the history
The console TUI's state model deals with two different types of IDs:

 - the actual `tracing` span IDs for an object (task, resource, or async
   op), which are *not* assigned sequentially, and may be reused over
   the program's lifetime, and
 - sequential "pretty" IDs assigned by the console, which are mapped to
   span IDs when an object is sent to the console. These are assigned
   based on separate counters for each type of object (so there can be
   both a task 1 and a resource 1, for example).

Remote span IDs are mapped to rewritten sequential IDs by the `Id` type,
which stores a map of span IDs to sequential IDs, and generates new
sequential IDs when a new span ID is recorded. The `Ids::id_for` method
takes a span ID and returns the corresponding sequential ID, and this
must be called before looking up or inserting an object by its remote
span ID.

Currently, *all* of these IDs are represented as `u64`s. This is
unfortunate, as it makes it very easy to accidentally introduce bugs
where the wrong kind of ID is used. For example, it's possible to
accidentally look up a task in the map of active tasks based on its span
ID on the remote application, which is likely to return `None` even if
there is a task with that span ID. PR #251 fixed one such ID-confusion
issue (where `WatchDetails` RPCs were performed with local, rewritten
task IDs rather than the remote's span ID for that task). However, it
would be better if we could just *not have* this class of issue.

This branch refactors the console's `state` module to make ID confusion
issues much harder to write. This is accomplished by adding an `Id`
newtype to represent our rewritten sequential IDs. `Ids::id_for` now
still takes a `u64` (as the remote span IDs are represented as `u64`s in
protobuf, so there's nothing we can do), but it returns an `Id` newtype.
Looking up or inserting objects in a state map now takes the `Id`
newtype. This ensures that all lookups are performed with sequential IDs
at the type level, and the only way to get a sequential ID is to ask the
`Ids` map for one.

Additionally, the `Id` type has a type parameter for the type of object
it identifies. This prevents additional issues where we might look up
the ID of a task in the map of resources, or similar.
  • Loading branch information
hawkw authored Jul 5, 2022
1 parent 9699300 commit 6baf72d
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 63 deletions.
32 changes: 19 additions & 13 deletions tokio-console/src/state/async_ops.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use crate::{
intern::{self, InternedStr},
state::{pb_duration, Attribute, Field, Ids, Metadata, Visibility},
state::{
id::{Id, Ids},
pb_duration,
resources::Resource,
tasks::Task,
Attribute, Field, Metadata, Visibility,
},
view,
};
use console_api as proto;
Expand All @@ -15,8 +21,8 @@ use tui::text::Span;

#[derive(Default, Debug)]
pub(crate) struct AsyncOpsState {
async_ops: HashMap<u64, Rc<RefCell<AsyncOp>>>,
ids: Ids,
async_ops: HashMap<Id<AsyncOp>, Rc<RefCell<AsyncOp>>>,
ids: Ids<AsyncOp>,
new_async_ops: Vec<AsyncOpRef>,
dropped_events: u64,
}
Expand All @@ -35,9 +41,9 @@ pub(crate) enum SortBy {

#[derive(Debug)]
pub(crate) struct AsyncOp {
num: u64,
num: Id<AsyncOp>,
parent_id: InternedStr,
resource_id: u64,
resource_id: Id<Resource>,
meta_id: u64,
source: InternedStr,
stats: AsyncOpStats,
Expand All @@ -56,7 +62,7 @@ struct AsyncOpStats {
last_poll_ended: Option<SystemTime>,
idle: Option<Duration>,
total: Option<Duration>,
task_id: Option<u64>,
task_id: Option<Id<Task>>,
task_id_str: InternedStr,
formatted_attributes: Vec<Vec<Span<'static>>>,
}
Expand Down Expand Up @@ -129,8 +135,8 @@ impl AsyncOpsState {
strings: &mut intern::Strings,
metas: &HashMap<u64, Metadata>,
update: proto::async_ops::AsyncOpUpdate,
resource_ids: &mut Ids,
task_ids: &mut Ids,
resource_ids: &mut Ids<Resource>,
task_ids: &mut Ids<Task>,
visibility: Visibility,
) {
let mut stats_update = update.stats_update;
Expand Down Expand Up @@ -227,19 +233,19 @@ impl AsyncOpsState {
}

impl AsyncOp {
pub(crate) fn id(&self) -> u64 {
pub(crate) fn id(&self) -> Id<AsyncOp> {
self.num
}

pub(crate) fn parent_id(&self) -> &str {
&self.parent_id
}

pub(crate) fn resource_id(&self) -> u64 {
pub(crate) fn resource_id(&self) -> Id<Resource> {
self.resource_id
}

pub(crate) fn task_id(&self) -> Option<u64> {
pub(crate) fn task_id(&self) -> Option<Id<Task>> {
self.stats.task_id
}

Expand Down Expand Up @@ -294,7 +300,7 @@ impl AsyncOpStats {
meta: &Metadata,
styles: &view::Styles,
strings: &mut intern::Strings,
task_ids: &mut Ids,
task_ids: &mut Ids<Task>,
) -> Self {
let mut pb = pb;

Expand Down Expand Up @@ -328,7 +334,7 @@ impl AsyncOpStats {
let task_id_str = strings.string(
task_id
.as_ref()
.map(u64::to_string)
.map(Id::<Task>::to_string)
.unwrap_or_else(|| "n/a".to_string()),
);
Self {
Expand Down
118 changes: 118 additions & 0 deletions tokio-console/src/state/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use std::{
any, cmp,
collections::hash_map::{Entry, HashMap},
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
};

pub(crate) struct Ids<T> {
next: u64,
map: HashMap<u64, Id<T>>,
}

/// A rewritten sequential ID.
///
/// This is distinct from the remote server's span ID, which may be reused and
/// is not sequential.
pub(crate) struct Id<T> {
id: u64,
_ty: PhantomData<fn(T)>,
}

// === impl Ids ===

impl<T> Ids<T> {
pub(crate) fn id_for(&mut self, span_id: u64) -> Id<T> {
match self.map.entry(span_id) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let id = Id {
id: self.next,
_ty: PhantomData,
};
entry.insert(id);
self.next = self.next.wrapping_add(1);
id
}
}
}
}

impl<T> Default for Ids<T> {
fn default() -> Self {
Self {
next: 1,
map: Default::default(),
}
}
}

impl<T> fmt::Debug for Ids<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ids")
.field("next", &self.next)
.field("map", &self.map)
.field("type", &format_args!("{}", any::type_name::<T>()))
.finish()
}
}

// === impl Id ===

impl<T> Clone for Id<T> {
#[inline]
fn clone(&self) -> Self {
Self {
id: self.id,
_ty: PhantomData,
}
}
}

impl<T> Copy for Id<T> {}

impl<T> fmt::Debug for Id<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = any::type_name::<T>();
let type_name = path.split("::").last().unwrap_or(path);
write!(f, "Id<{}>({})", type_name, self.id)
}
}

impl<T> fmt::Display for Id<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.id, f)
}
}

impl<T> Hash for Id<T> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
state.write_u64(self.id);
}
}

impl<T> PartialEq for Id<T> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}

impl<T> Eq for Id<T> {}

impl<T> cmp::Ord for Id<T> {
#[inline]
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.id.cmp(&other.id)
}
}

impl<T> cmp::PartialOrd for Id<T> {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
36 changes: 4 additions & 32 deletions tokio-console/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use console_api as proto;
use std::{
cell::RefCell,
cmp::Ordering,
collections::hash_map::{Entry, HashMap},
collections::HashMap,
convert::{TryFrom, TryInto},
fmt,
rc::Rc,
Expand All @@ -22,9 +22,12 @@ use tui::{

pub mod async_ops;
pub mod histogram;
pub mod id;
pub mod resources;
pub mod tasks;

pub(crate) use self::id::Id;

pub(crate) type DetailsRef = Rc<RefCell<Option<Details>>>;

#[derive(Default, Debug)]
Expand Down Expand Up @@ -80,12 +83,6 @@ pub(crate) struct Attribute {
unit: Option<String>,
}

#[derive(Debug)]
pub(crate) struct Ids {
next: u64,
map: HashMap<u64, u64>,
}

impl State {
pub(crate) fn with_retain_for(mut self, retain_for: Option<Duration>) -> Self {
self.retain_for = retain_for;
Expand Down Expand Up @@ -496,31 +493,6 @@ impl Attribute {
}
}

// === impl Ids ===

impl Ids {
pub(crate) fn id_for(&mut self, span_id: u64) -> u64 {
match self.map.entry(span_id) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let id = self.next;
entry.insert(id);
self.next = self.next.wrapping_add(1);
id
}
}
}
}

impl Default for Ids {
fn default() -> Self {
Self {
next: 1,
map: Default::default(),
}
}
}

fn truncate_registry_path(s: String) -> String {
use once_cell::sync::OnceCell;
use regex::Regex;
Expand Down
23 changes: 14 additions & 9 deletions tokio-console/src/state/resources.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use crate::intern::{self, InternedStr};
use crate::state::{format_location, Attribute, Field, Ids, Metadata, Visibility};
use crate::state::{
format_location,
id::{Id, Ids},
Attribute, Field, Metadata, Visibility,
};
use crate::view;
use console_api as proto;
use std::{
Expand All @@ -13,8 +17,8 @@ use tui::{style::Color, text::Span};

#[derive(Default, Debug)]
pub(crate) struct ResourcesState {
resources: HashMap<u64, Rc<RefCell<Resource>>>,
pub(crate) ids: Ids,
resources: HashMap<Id<Resource>, Rc<RefCell<Resource>>>,
pub(crate) ids: Ids<Resource>,
new_resources: Vec<ResourceRef>,
dropped_events: u64,
}
Expand All @@ -41,7 +45,7 @@ pub(crate) struct Resource {
///
/// This is NOT the `tracing::span::Id` for the resource's `tracing` span on the
/// remote.
num: u64,
num: Id<Resource>,
/// The `tracing::span::Id` on the remote process for this resource's span.
///
/// This is used when requesting a resource details stream.
Expand Down Expand Up @@ -119,7 +123,7 @@ impl ResourcesState {
self.new_resources.drain(..)
}

pub(crate) fn resource(&self, id: u64) -> Option<ResourceRef> {
pub(crate) fn resource(&self, id: Id<Resource>) -> Option<ResourceRef> {
self.resources.get(&id).map(Rc::downgrade)
}

Expand All @@ -131,11 +135,12 @@ impl ResourcesState {
update: proto::resources::ResourceUpdate,
visibility: Visibility,
) {
let parents: HashMap<u64, ResourceRef> = update
let parents: HashMap<Id<Resource>, ResourceRef> = update
.new_resources
.iter()
.filter_map(|resource| {
let parent_id = resource.parent_resource_id?.id;
let span_id = resource.parent_resource_id?.id;
let parent_id = self.ids.id_for(span_id);
let parent = self.resource(parent_id)?;
Some((parent_id, parent))
})
Expand Down Expand Up @@ -196,7 +201,7 @@ impl ResourcesState {
let parent_id = strings.string(
parent_id
.as_ref()
.map(u64::to_string)
.map(Id::<Resource>::to_string)
.unwrap_or_else(|| "n/a".to_string()),
);

Expand Down Expand Up @@ -262,7 +267,7 @@ impl ResourcesState {
}

impl Resource {
pub(crate) fn id(&self) -> u64 {
pub(crate) fn id(&self) -> Id<Resource> {
self.num
}

Expand Down
Loading

0 comments on commit 6baf72d

Please sign in to comment.