Skip to content

Commit 8a4e042

Browse files
mariusaefacebook-github-bot
authored andcommitted
attrs: require that values implement AttrValue (#1288)
Summary: Pull Request resolved: #1288 We introduce `AttrValue`, which must be implemented by all value types in attribute dictionary. AttrValue 1) gathers all the requirements (Send, Serialize, Named, ...) of attribute values, and 2) introduces `AttrValue::display` and `AttrValue::parse`, to be used when attribute values are presented to humans. In most cases, `display` and `parse` can be derived from the type's `ToString` and `FromStr` implementations. The change includes a derive macro to bridge these. ghstack-source-id: 311304360 exported-using-ghexport Reviewed By: shayne-fletcher Differential Revision: D82921321 fbshipit-source-id: bc1c6ea9872db4852f61b833e08dfc54951f2c2d
1 parent 131cdbc commit 8a4e042

File tree

9 files changed

+709
-89
lines changed

9 files changed

+709
-89
lines changed

hyperactor/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async-trait = "0.1.86"
4242
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
4343
bincode = "1.3.3"
4444
bytes = { version = "1.10", features = ["serde"] }
45+
chrono = { version = "0.4.41", features = ["clock", "serde", "std"], default-features = false }
4546
cityhasher = "0.1.0"
4647
clap = { version = "4.5.42", features = ["derive", "env", "string", "unicode", "wrap_help"] }
4748
crc32fast = "1.4"
@@ -54,6 +55,7 @@ erased-serde = "0.3.27"
5455
fastrand = "2.1.1"
5556
futures = { version = "0.3.31", features = ["async-await", "compat"] }
5657
hostname = "0.3"
58+
humantime = "2.1"
5759
hyperactor_macros = { version = "0.0.0", path = "../hyperactor_macros" }
5860
hyperactor_telemetry = { version = "0.0.0", path = "../hyperactor_telemetry" }
5961
inventory = "0.3.8"

hyperactor/src/attrs.rs

Lines changed: 155 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//!
1111
//! This module provides `Attrs`, a type-safe dictionary that can store heterogeneous values
1212
//! and serialize/deserialize them using serde. All stored values must implement
13-
//! `Serialize + DeserializeOwned` to ensure the entire dictionary can be serialized.
13+
//! `AttrValue` to ensure the entire dictionary can be serialized.
1414
//!
1515
//! Keys are automatically registered at compile time using the `declare_attrs!` macro and the
1616
//! inventory crate, eliminating the need for manual registry management.
@@ -100,6 +100,8 @@ use std::ops::Index;
100100
use std::ops::IndexMut;
101101
use std::sync::LazyLock;
102102

103+
use chrono::DateTime;
104+
use chrono::Utc;
103105
use erased_serde::Deserializer as ErasedDeserializer;
104106
use erased_serde::Serialize as ErasedSerialize;
105107
use serde::Deserialize;
@@ -125,8 +127,12 @@ pub struct AttrKeyInfo {
125127
/// Deserializer function that deserializes directly from any deserializer
126128
pub deserialize_erased:
127129
fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
128-
// Meta-attributes.
130+
/// Meta-attributes.
129131
pub meta: &'static LazyLock<Attrs>,
132+
/// Display an attribute value using AttrValue::display.
133+
pub display: fn(&dyn SerializableValue) -> String,
134+
/// Parse an attribute value using AttrValue::parse.
135+
pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
130136
}
131137

132138
inventory::collect!(AttrKeyInfo);
@@ -192,7 +198,7 @@ impl<T: 'static> Clone for Key<T> {
192198
impl<T: 'static> Copy for Key<T> {}
193199

194200
// Enable attr[key] syntax.
195-
impl<T: Send + Sync + Serialize + DeserializeOwned + Named + 'static> Index<Key<T>> for Attrs {
201+
impl<T: AttrValue> Index<Key<T>> for Attrs {
196202
type Output = T;
197203

198204
fn index(&self, key: Key<T>) -> &Self::Output {
@@ -202,14 +208,113 @@ impl<T: Send + Sync + Serialize + DeserializeOwned + Named + 'static> Index<Key<
202208

203209
// TODO: separately type keys with defaults, so that we can statically enforce that indexmut is only
204210
// called on keys with defaults.
205-
impl<T: Send + Sync + Serialize + DeserializeOwned + Named + Clone + 'static> IndexMut<Key<T>>
206-
for Attrs
207-
{
211+
impl<T: AttrValue> IndexMut<Key<T>> for Attrs {
208212
fn index_mut(&mut self, key: Key<T>) -> &mut Self::Output {
209213
self.get_mut(key).unwrap()
210214
}
211215
}
212216

217+
/// This trait must be implemented by all attribute values. In addition to enforcing
218+
/// the supertrait `Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone`,
219+
/// `AttrValue` requires that the type be representable in "display" format.
220+
///
221+
/// `AttrValue` includes its own `display` and `parse` so that behavior can be tailored
222+
/// for attribute purposes specifically, allowing common types like `Duration` to be used
223+
/// without modification.
224+
///
225+
/// This crate includes a derive macro for AttrValue, which uses the type's
226+
/// `std::string::ToString` for display, and `std::str::FromStr` for parsing.
227+
pub trait AttrValue:
228+
Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone + 'static
229+
{
230+
/// Display the value, typically using [`std::fmt::Display`].
231+
/// This is called to show the output in human-readable form.
232+
fn display(&self) -> String;
233+
234+
/// Parse a value from a string, typically using [`std::str::FromStr`].
235+
fn parse(value: &str) -> Result<Self, anyhow::Error>;
236+
}
237+
238+
/// Macro to implement AttrValue for types that implement ToString and FromStr.
239+
///
240+
/// This macro provides a convenient way to implement AttrValue for types that already
241+
/// have string conversion capabilities through the standard ToString and FromStr traits.
242+
///
243+
/// # Usage
244+
///
245+
/// ```ignore
246+
/// impl_attrvalue!(i32, u64, f64);
247+
/// ```
248+
///
249+
/// This will generate AttrValue implementations for i32, u64, and f64 that use
250+
/// their ToString and FromStr implementations for display and parsing.
251+
#[macro_export]
252+
macro_rules! impl_attrvalue {
253+
($($ty:ty),+ $(,)?) => {
254+
$(
255+
impl $crate::attrs::AttrValue for $ty {
256+
fn display(&self) -> String {
257+
self.to_string()
258+
}
259+
260+
fn parse(value: &str) -> Result<Self, anyhow::Error> {
261+
value.parse().map_err(|e| anyhow::anyhow!("failed to parse {}: {}", stringify!($ty), e))
262+
}
263+
}
264+
)+
265+
};
266+
}
267+
268+
// pub use impl_attrvalue;
269+
270+
// Implement AttrValue for common standard library types
271+
impl_attrvalue!(
272+
bool,
273+
i8,
274+
i16,
275+
i32,
276+
i64,
277+
i128,
278+
isize,
279+
u8,
280+
u16,
281+
u32,
282+
u64,
283+
u128,
284+
usize,
285+
f32,
286+
f64,
287+
String,
288+
std::net::IpAddr,
289+
std::net::Ipv4Addr,
290+
std::net::Ipv6Addr,
291+
crate::ActorId,
292+
ndslice::Shape,
293+
ndslice::Point,
294+
);
295+
296+
impl AttrValue for std::time::Duration {
297+
fn display(&self) -> String {
298+
humantime::format_duration(*self).to_string()
299+
}
300+
301+
fn parse(value: &str) -> Result<Self, anyhow::Error> {
302+
Ok(humantime::parse_duration(value)?)
303+
}
304+
}
305+
306+
impl AttrValue for std::time::SystemTime {
307+
fn display(&self) -> String {
308+
let datetime: DateTime<Utc> = self.clone().into();
309+
datetime.to_rfc3339()
310+
}
311+
312+
fn parse(value: &str) -> Result<Self, anyhow::Error> {
313+
let datetime = DateTime::parse_from_rfc3339(value)?;
314+
Ok(datetime.into())
315+
}
316+
}
317+
213318
// Internal trait for type-erased serialization
214319
#[doc(hidden)]
215320
pub trait SerializableValue: Send + Sync {
@@ -223,7 +328,7 @@ pub trait SerializableValue: Send + Sync {
223328
fn cloned(&self) -> Box<dyn SerializableValue>;
224329
}
225330

226-
impl<T: Serialize + Send + Sync + Clone + 'static> SerializableValue for T {
331+
impl<T: AttrValue> SerializableValue for T {
227332
fn as_any(&self) -> &dyn Any {
228333
self
229334
}
@@ -245,7 +350,7 @@ impl<T: Serialize + Send + Sync + Clone + 'static> SerializableValue for T {
245350
///
246351
/// This dictionary stores key-value pairs where:
247352
/// - Keys are type-safe and must be predefined with their associated types
248-
/// - Values must implement `Send + Sync + Serialize + DeserializeOwned + Named + 'static`
353+
/// - Values must implement [`AttrValue`]
249354
/// - The entire dictionary can be serialized to/from JSON automatically
250355
///
251356
/// # Type Safety
@@ -271,20 +376,11 @@ impl Attrs {
271376
}
272377

273378
/// Set a value for the given key.
274-
pub fn set<T: Send + Sync + Serialize + DeserializeOwned + Named + Clone + 'static>(
275-
&mut self,
276-
key: Key<T>,
277-
value: T,
278-
) {
379+
pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
279380
self.values.insert(key.name, Box::new(value));
280381
}
281382

282-
fn maybe_set_from_default<
283-
T: Send + Sync + Serialize + DeserializeOwned + Named + Clone + 'static,
284-
>(
285-
&mut self,
286-
key: Key<T>,
287-
) {
383+
fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
288384
if self.contains_key(key) {
289385
return;
290386
}
@@ -294,10 +390,7 @@ impl Attrs {
294390

295391
/// Get a value for the given key, returning None if not present. If the key has a default value,
296392
/// that is returned instead.
297-
pub fn get<T: Send + Sync + Serialize + DeserializeOwned + Named + 'static>(
298-
&self,
299-
key: Key<T>,
300-
) -> Option<&T> {
393+
pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
301394
self.values
302395
.get(key.name)
303396
.and_then(|value| value.as_any().downcast_ref::<T>())
@@ -306,30 +399,21 @@ impl Attrs {
306399

307400
/// Get a mutable reference to a value for the given key. If the key has a default value, it is
308401
/// first set, and then returned as a mutable reference.
309-
pub fn get_mut<T: Send + Sync + Serialize + DeserializeOwned + Named + Clone + 'static>(
310-
&mut self,
311-
key: Key<T>,
312-
) -> Option<&mut T> {
402+
pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
313403
self.maybe_set_from_default(key);
314404
self.values
315405
.get_mut(key.name)
316406
.and_then(|value| value.as_any_mut().downcast_mut::<T>())
317407
}
318408

319409
/// Remove a value for the given key, returning it if present.
320-
pub fn remove<T: Send + Sync + Serialize + DeserializeOwned + Named + 'static>(
321-
&mut self,
322-
key: Key<T>,
323-
) -> bool {
410+
pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
324411
// TODO: return value (this is tricky because of the type erasure)
325412
self.values.remove(key.name).is_some()
326413
}
327414

328415
/// Checks if the given key exists in the dictionary.
329-
pub fn contains_key<T: Send + Sync + Serialize + DeserializeOwned + Named + 'static>(
330-
&self,
331-
key: Key<T>,
332-
) -> bool {
416+
pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
333417
self.values.contains_key(key.name)
334418
}
335419

@@ -533,6 +617,19 @@ macro_rules! const_ascii_lowercase {
533617
}};
534618
}
535619

620+
/// Macro to check check that a trait is implemented, generating a
621+
/// nice error message if it isn't.
622+
#[doc(hidden)]
623+
#[macro_export]
624+
macro_rules! assert_impl {
625+
($ty:ty, $trait:path) => {
626+
const _: fn() = || {
627+
fn check<T: $trait>() {}
628+
check::<$ty>();
629+
};
630+
};
631+
}
632+
536633
/// Declares attribute keys using a lazy_static! style syntax.
537634
///
538635
/// # Syntax
@@ -595,6 +692,8 @@ macro_rules! declare_attrs {
595692

596693
// Handle single attribute key with default value and meta attributes
597694
(@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
695+
$crate::assert_impl!($type, $crate::attrs::AttrValue);
696+
598697
// Create a static default value
599698
$crate::paste! {
600699
static [<$name _DEFAULT>]: $type = $default;
@@ -611,6 +710,8 @@ macro_rules! declare_attrs {
611710

612711
$(#[$attr])*
613712
$vis static $name: $crate::attrs::Key<$type> = {
713+
$crate::assert_impl!($type, $crate::attrs::AttrValue);
714+
614715
const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
615716
const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
616717
$crate::paste! {
@@ -635,12 +736,22 @@ macro_rules! declare_attrs {
635736
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
636737
},
637738
meta: $crate::paste! { &[<$name _META_ATTRS>] },
739+
display: |value: &dyn $crate::attrs::SerializableValue| {
740+
let value = value.as_any().downcast_ref::<$type>().unwrap();
741+
$crate::attrs::AttrValue::display(value)
742+
},
743+
parse: |value: &str| {
744+
let value: $type = $crate::attrs::AttrValue::parse(value)?;
745+
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
746+
},
638747
}
639748
}
640749
};
641750

642751
// Handle single attribute key without default value but with meta attributes
643752
(@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
753+
$crate::assert_impl!($type, $crate::attrs::AttrValue);
754+
644755
$crate::paste! {
645756
static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
646757
std::sync::LazyLock::new(|| {
@@ -676,6 +787,14 @@ macro_rules! declare_attrs {
676787
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
677788
},
678789
meta: $crate::paste! { &[<$name _META_ATTRS>] },
790+
display: |value: &dyn $crate::attrs::SerializableValue| {
791+
let value = value.as_any().downcast_ref::<$type>().unwrap();
792+
$crate::attrs::AttrValue::display(value)
793+
},
794+
parse: |value: &str| {
795+
let value: $type = $crate::attrs::AttrValue::parse(value)?;
796+
Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
797+
},
679798
}
680799
}
681800
};
@@ -693,7 +812,7 @@ mod tests {
693812
attr TEST_TIMEOUT: Duration;
694813
attr TEST_COUNT: u32;
695814
@meta(TEST_COUNT = 42)
696-
attr TEST_NAME: String;
815+
pub attr TEST_NAME: String;
697816
}
698817

699818
#[test]

hyperactor/src/config.rs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ pub mod global {
217217
use std::marker::PhantomData;
218218

219219
use super::*;
220+
use crate::attrs::AttrValue;
220221
use crate::attrs::Key;
221222

222223
/// Global configuration instance, initialized from environment variables.
@@ -259,17 +260,7 @@ pub mod global {
259260

260261
/// Get a key from the global configuration. Currently only available for Copy types.
261262
/// `get` assumes that the key has a default value.
262-
pub fn get<
263-
T: Send
264-
+ Sync
265-
+ Copy
266-
+ serde::Serialize
267-
+ serde::de::DeserializeOwned
268-
+ crate::data::Named
269-
+ 'static,
270-
>(
271-
key: Key<T>,
272-
) -> T {
263+
pub fn get<T: AttrValue + Copy>(key: Key<T>) -> T {
273264
*CONFIG.read().unwrap().get(key).unwrap()
274265
}
275266

@@ -300,16 +291,7 @@ pub mod global {
300291
/// Create a configuration override that will be restored when the guard is dropped.
301292
///
302293
/// The returned guard must not outlive this ConfigLock.
303-
pub fn override_key<
304-
'a,
305-
T: Send
306-
+ Sync
307-
+ serde::Serialize
308-
+ serde::de::DeserializeOwned
309-
+ crate::data::Named
310-
+ Clone
311-
+ 'static,
312-
>(
294+
pub fn override_key<'a, T: AttrValue>(
313295
&'a self,
314296
key: crate::attrs::Key<T>,
315297
value: T,

0 commit comments

Comments
 (0)