-
Notifications
You must be signed in to change notification settings - Fork 277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[refactor] #3501: Rewrite macros for generating configuration #3512
Conversation
7a38c9e
to
420b5ae
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like new approach - ConfigurationBuilder
is clearer than ConfigurationProxy
, and macro generates default fallback, allowing to define only required fields in the config, which solves:
config/src/sumeragi.rs
Outdated
/// Custom deserializer that ensures that `trusted_peers` only | ||
/// contains unique `PeerId`'s. | ||
/// | ||
/// # Errors | ||
/// - Peer Ids not unique, | ||
/// - Not a sequence (array) | ||
fn deserialize_unique_trusted_peers<'de, D>(deserializer: D) -> Result<HashSet<PeerId>, D::Error> | ||
fn deserialize_unique_trusted_peers<'de, D>(deserializer: D) -> Result<Vec<PeerId>, D::Error> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Peers traversal order determinism doesn't matter anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. I'm not sure if the order was preserved in the previous impl. I have fixed it now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nvm, I was confused and messed HashSet
with BTreeSet
. The order doesn't matter for HashSet
.
However, the method is called "deserialise unique trusted peers", so it does make sense to return a set, not a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
order of peers is relevant for consensus so it should be a vector
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, but what about uniqueness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the body of the function throws an error if they are not unique
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should add validation for the setter method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the body of the function throws an error if they are not unique
It's better when validation result is expressed as a type that cannot be created without that validation (parse-not-validate).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not touched upon in the documentation. Semantically this is a Set
. The functional programmer would say "add a transparent newtype with deduplication semantics and iter
", like @0x009922 suggests.
The imperative programmer in me thinks "Just use vector, people working on consensus have to read the docs anyway". If you're feeling fancy, call it
type PeerSet = Vec<PeerId>
@@ -162,9 +159,7 @@ impl WorldStateView { | |||
#[inline] | |||
pub fn new(world: World, kura: Arc<Kura>) -> Self { | |||
// Added to remain backward compatible with other code primary in tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe, make this function #[cfg(test)]
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately no because it's used in integration benches which are compiled into a different crate. cfg(test)
only works for unit tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One workaround which I don't think we ever considered was separating tests by debug
status. We're not currently running tests in release
, and if we did, we shouldn't be using private API anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One workaround which I don't think we ever considered was separating tests by debug status. We're not currently running tests in release, and if we did, we shouldn't be using private API anyway.
this might be a good idea. We could rely on debug_assertions
to separate tests that test API that is kinda private
912cf95
to
d9935fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You say that it doesn't mean that the field should exist in the final configuration. Instead, it means that the user should provide it anyway. In other words, such a field doesn't have a default value, and it can't be None
in the final configuration.
So, now it works this way (as I understand):
#[derive(Configuration)]
struct Config {
// has default value
foo: u32,
// this fields doesn't have a default value
#[config(required)]
bar: u32,
// some field you want to be completely optional, even without a default value
// not possible now?
// baz: Option<u32>
}
impl Config {
const DEFAULT_FOO: u32 = 512;
}
Expanded:
struct ConfigBuilder {
foo: Option<u32>,
bar: Option<u32>
}
struct ConfigBuilder {
foo: u32,
bar: u32
}
Compare with this:
#[derive(Configuration)]
struct Config {
// this field is not required for the user to provide, because it has a default fallback
#[config(default_value = "512")]
foo: u32,
// this field doesn't have a default fallback, so user should provide it in some way
bar: u32,
// neither a default-fallback-able field and a required field
// not sure it is needed now though
#[config(optional)]
baz: u32
}
Expanded:
struct ConfigBuilder {
foo: Option<u32>,
bar: Option<u32>,
baz: Option<u32>
}
struct Config {
foo: u32,
bar: u32,
baz: Option<u32>
}
impl Config {
const DEFAULT_FOO: u32 = 512;
}
impl ConfigBuilder {
fn build(self) -> Result<Config> {
Ok(Config {
foo: self.foo.unwrap_or(Config::DEFAULT_FOO),
bar: self.bar.ok_or_else(|| MissingField("bar"))?,
baz: self.baz
})
}
}
In the second option:
-
You don't have to manually specify
DEFAULT_FOO
- it is generated by macro fromdefault_value
attr. -
You have a default value specified right next (well, before) to the field itself
-
You don't need to specify that the field is required: it is required by definition unless it doesn't have a default value.
-
You can have a completely optional field. I am not sure that we need it now, but you can painlessly add it later.
-
You have default fallback in
ConfigBuilder::build()
. Current way to have defaults is to mergeConfigBuilder::default()
withConfigBuilder::from_file()
withConfigBuilder::from_env()
and then finally call.build()
:fn build(self) -> Result<Config> { Ok(Config { foo: self.foo.ok_or_else(|| MissingField("foo")?, // ... }) }
Imagine that
foo
has a default value. Thus, this error will never be thrown, semantically, right? Field with a default value could never be missing.However, if you simply merge
::from_file()
with::from_env()
and then fallback to default in.build()
:fn build(self) -> Result<Config> { Ok(Config { foo: self.foo.unwrap_or(Config::DEFAULT_FOO), // ... }) }
you don't even need to check for the field existence, you simply fallback. This way more clearly expresses that a field with a default value could not throw a
MissingField
error.
config/derive/src/proxy.rs
Outdated
/// Build [`Configuration`] | ||
/// | ||
/// # Errors | ||
/// | ||
/// - if missing required fields | ||
pub fn build(self) -> Result<#parent_ident, ::iroha_config::Error> { | ||
let config = Self::default().override_with(self); | ||
|
||
Ok(#parent_ident { #( | ||
#field_names: config.#field_names.ok_or( | ||
::iroha_config::Error::MissingField(stringify!(#field_names)) | ||
)? ),* | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that the final Configuration
should not have any None
s, otherwise you will get a missing field error. Is it the case? Isn't it possible that the final configuration could have an empty field that is not defined neither by user and by defaults?
Also, it would be nice to refactor code so that fields with default values could never throw a MissingField
on a static-level. See my root review comment with an explanation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These questions feel like they need to be phrased as tests.
One thing we found was useful was pair programming. You could contribute those directly to @mversic 's fork and expedite the invariant discussion.
Also it might close: |
9b6f2c2
to
b041e9d
Compare
cli/src/samples.rs
Outdated
.set_max_transactions_in_block(2) | ||
.set_trusted_peers(peers) | ||
.build() | ||
.expect("Valid"); | ||
let genesis = iroha_config::genesis::ConfigurationBuilder::default() | ||
.set_account_private_key(Some(private_key)) | ||
.set_account_public_key(public_key) | ||
.build() | ||
.expect("Valid"); | ||
let block_sync = iroha_config::block_sync::ConfigurationBuilder::default() | ||
.set_block_batch_size(1) | ||
.set_gossip_period_ms(500) | ||
.build() | ||
.expect("Valid"); | ||
|
||
iroha_config::iroha::ConfigurationBuilder::default() | ||
.set_public_key(public_key.clone()) | ||
.set_private_key(private_key.clone()) | ||
.set_sumeragi(sumeragi) | ||
.set_block_sync(block_sync) | ||
.set_genesis(genesis) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relevant for #3528.
cli/src/samples.rs
Outdated
.set_max_transactions_in_block(2) | ||
.set_trusted_peers(peers) | ||
.build() | ||
.expect("Valid"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.expect("Valid"); | |
.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.expect("Valid"); | |
.expect("Max transactions in block = 2 is sensible. Trusted peers could be not sensible, which is why this function should have Returned `Result`, but we might as well panic here. These are the only two required fields."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#3528 is to avoid this issue of having to justify all the values in the panic message, while retaining the strict guideline of only panicking in cases where the error is not recoverable or unreachable.
cli/src/samples.rs
Outdated
/// | ||
/// # Panics | ||
/// - when [`KeyPair`] generation fails (rare case). | ||
/// Get a sample Iroha configuration. Use [`get_trusted_peers`] to populate `trusted_peers` if in doubt. | ||
pub fn get_config(trusted_peers: HashSet<PeerId>, key_pair: Option<KeyPair>) -> Configuration { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API inconsistency. For trusted peers order either matters or it doesn't. Personally, just accept a PeerSet
and make it a newtype builder with SmallVec
backing.
client/src/lib.rs
Outdated
|
||
let account_id = "alice@wonderland" | ||
.parse() | ||
.expect("This account ID should be valid"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.expect("This account ID should be valid"); | |
.expect("`account_id` is invalid, see https://hyperledger.github.io/iroha-2-docs/guide/rust.html#_4-registering-an-account. "); |
Ideally this should point to an API doc, but that's blocked by #1639.
client/src/lib.rs
Outdated
.set_private_key(private_key) | ||
.set_account_id(account_id) | ||
.build() | ||
.expect("Client config should build as all required fields were provided") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not actionable:
Ideally, point to API doc that shows which fields are required. The error message will tell us the rest.
tools/kagami/src/main.rs
Outdated
.add_parameter(WASM_MAX_MEMORY, DEFAULT_MAX_MEMORY)? | ||
.add_parameter( | ||
WSV_ACCOUNT_METADATA_LIMITS, | ||
WsvConfiguration::DEFAULT_ACCOUNT_METADATA_LIMITS, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider use WsvConfiguration::*
.
Unlike C++, Rust's imports are checked for conflicts and are scoped, so we can get away with shortening qualified paths more aggressively.
core/test_network/src/lib.rs
Outdated
@@ -818,8 +818,8 @@ impl TestClient for Client { | |||
fn test_with_key(api_url: &SocketAddr, telemetry_url: &SocketAddr, keys: KeyPair) -> Self { | |||
let mut configuration = ClientConfiguration::test(api_url, telemetry_url); | |||
let (public_key, private_key) = keys.into(); | |||
configuration.public_key = public_key; | |||
configuration.private_key = private_key; | |||
configuration.set_public_key(public_key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider set_key_pair((public_key, private_key))
. It saves one line, but in a lot of places.
@@ -162,9 +159,7 @@ impl WorldStateView { | |||
#[inline] | |||
pub fn new(world: World, kura: Arc<Kura>) -> Self { | |||
// Added to remain backward compatible with other code primary in tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One workaround which I don't think we ever considered was separating tests by debug
status. We're not currently running tests in release
, and if we did, we shouldn't be using private API anyway.
d2688f1
to
119734d
Compare
Fixing #3461 was a trivial one-line change. I would instead focus on getting good defaults. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About optional fields. Now it is possible to define them as Option<..>
. In the ConfigurationBuilder
they become Option<Option<..>>
. Could we avoid it?
config/src/sumeragi.rs
Outdated
#[config(required)] | ||
key_pair: KeyPair, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You said that this PR closes #3504, but this field is still in the user-facing configuration, and user still should not provide it. This field simply should not be in ConfigurationBuilder
.
What does #[view(ignore)]
means? Why isn't it specified under #[config(...)]
?
/// `Kura` configuration | ||
#[config(inner)] | ||
pub kura: kura::Configuration, | ||
#[config(default = "kura::Configuration::default()")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use config(nested)
and move this boilerplate into the macro.
} | ||
} | ||
#[cfg(all(feature = "tokio-console", not(feature = "no-tokio-console")))] | ||
#[config(default = "= "127.0.0.1:5555"")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default = "= "127.0.0.1:5555""
?
/// Maximum number of blocks to write into a single storage file. | ||
pub blocks_per_storage_file: NonZeroU64, | ||
// SAFETY: Value is not zero | ||
#[config(default = "unsafe { NonZeroU64::new_unchecked(1000_u64) }")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer const panic!
. Also, for complex defaults you can define a separate const (AFAIU):
const DEFAULT_BLOCKS_PER_STORAGE: NonZeroU64 = match NonZeroU64::new(1000) {
Some(x) => x,
None => unreachable!()
};
// ...
#[config(default = "DEFAULT_BLOCKS_PER_STORAGE")]
} | ||
|
||
/// Override [`Self`] with values from another [`Self`] | ||
pub fn override_with(&mut self, other: Self) -> &mut Self { #( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub fn override_with(&mut self, other: Self) -> &mut Self { #( | |
pub fn merge(mut self, other: Self) -> Self { #( |
impl From<#parent_ident> for #builder_ident { | ||
fn from(source: #parent_ident) -> Self { | ||
Self { #( #field_names: Some(source.#field_names) ),* } | ||
} | ||
set_field | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about optional fields, which might be None
in the final configuration?
Signed-off-by: Marin Veršić <[email protected]>
Not actual. |
Description
Linked issue
Closes #3501 #3504
Benefits
Checklist