-
Notifications
You must be signed in to change notification settings - Fork 999
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
swarm: Deprecate Swarm::with_XYZ
and enforce creation via SwarmBuilder
#3186
Comments
Using the builder pattern to construct a
With that in mind I am advocating against removing
In case we want to improve here, I would suggest going the opposite route, namely to enforce the creation of a |
That sounds reasonable to me! |
FWIW: Making them reconfigurable wasn't the goal of this, I also don't see why anyone would want it but that doesn't mean we can't allow it as long as the resulting behaviour is sane (which is ensured by the linked PRs). |
I like the idea of enforcing creation of |
SwarmBuilder
Swarm::new
and enforce creation via SwarmBuilder
Swarm::new
and enforce creation via SwarmBuilder
Swarm::with_XYZ
and enforce creation via SwarmBuilder
I want to tackle it. I'd like to align on the approach first, For the struct pub struct SwarmBuilder<TBehaviour> {
local_peer_id: PeerId,
transport: transport::Boxed<(PeerId, StreamMuxerBox)>,
behaviour: TBehaviour,
pool_config: PoolConfig,
connection_limits: ConnectionLimits,
} I will make all fields configurable and leave the current configuration functions in place, such as, pub fn notify_handler_buffer_size(mut self, n: NonZeroUsize) -> Self {
self.pool_config = self.pool_config.with_notify_handler_buffer_size(n);
self
} Mark items such as the following deprecated. I will not change the visibility; it's a breaking change unless you're OK with introducing such changes immediately. pub fn with_executor(
transport: transport::Boxed<(PeerId, StreamMuxerBox)>,
behaviour: TBehaviour,
local_peer_id: PeerId,
executor: impl Executor + Send + 'static,
) -> Self Additionally, convenience functions such as below in impl<TBehaviour> Swarm<TBehaviour>
where
TBehaviour: NetworkBehaviour,
{
/// Builds a new `Swarm` with a provided executor.
pub fn with_executor(
transport: transport::Boxed<(PeerId, StreamMuxerBox)>,
behaviour: TBehaviour,
local_peer_id: PeerId,
executor: impl Executor + Send + 'static,
) -> Self {
SwarmBuilder::with_executor(transport, behaviour, local_peer_id, executor).build()
} |
Thanks!
A builder pattern typically starts with all mandatory fields and then allows you to configure optional fields. PeerId, transport and behaviour are mandatory in our case, so those shouldn't be configurable.
Those functions on Does that make sense? This should be another PR though as it will likely be breaking unless we introduce new functions and deprecate these.
Yes those should be deprecated and eventually removed. |
Ok, let me give you a taste what it would like impl<TBehaviour> SwarmBuilder<TBehaviour>
where
TBehaviour: NetworkBehaviour,
{
/// Creates a new [`SwarmBuilder`] from the given transport, behaviour and local peer ID. The
/// `Swarm` with its underlying `Network` is obtained via [`SwarmBuilder::build`].
///
/// ## ⚠️ Performance warning
/// BY default, all connections will be polled on the current task, thus quite bad performance
/// characteristics should be expected. Whenever possible, use an executor which could be set
/// either selected from available executors: [`SwarmBuilder::tokio_executor`] or
/// [`SwarmBuilder::async_std_executor`]. Alternatively, a custom executor can be set with
/// [`SwarmBuilder::executor`].
pub fn new(
transport: transport::Boxed<(PeerId, StreamMuxerBox)>,
behaviour: TBehaviour,
local_peer_id: PeerId,
) -> Self {
Self {
local_peer_id,
transport,
behaviour,
pool_config: PoolConfig::new(None),
connection_limits: Default::default(),
}
}
/// Sets executor of this [`SwarmBuilder`] to a given value.
pub fn executor(mut self, executor: impl Executor + Send + 'static) -> Self {
self.pool_config = PoolConfig::new(Some(Box::new(executor)));
self
}
/// Set executor for this [`SwarmBuilder`] to the `tokio` executor.
#[cfg(all(
feature = "tokio",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown"))
))]
pub fn tokio_executor(mut self) -> Self {
self.executor(crate::executor::TokioExecutor)
}
/// Set executor for this [`SwarmBuilder`] to the `async-std` executor.
#[cfg(all(
feature = "async-std",
not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown"))
))]
pub fn async_std_executor(mut self) -> Self {
self.executor(crate::executor::AsyncStdExecutor)
}
...
} So the building will be pub fn notify_handler_buffer_size(mut self, n: NonZeroUsize) -> Self {
self.pool_config = self.pool_config.with_notify_handler_buffer_size(n);
self
} |
Interesting idea, thank you for sharing. One of the reasons we introduced these runtime-based builder methods is that for tokio for example, it is required that the connection tasks run on the tokio runtime, otherwise they will panic at runtime. Thus, I think it should not be optional to call One thing we can do is make I don't mind whether we:
But I'd like to have some form of guidance in the type-system here otherwise, using Let me know what you think! |
Listing It seems to me that having a state type on However, regardless of the selected approach, the type system won't prevent users from creating a swarm with, f.e., async std in the tokio context. This coupling is external to the swarm lib. I don't believe it is enforcable. |
The starting point for this issue is that we have this API on
That is true. I'd expect users to only enable either the |
Understood. All right. As far as I can see, I can do one of the following:
IMO, the first approach is the cleanest and follows the builder pattern to the letter. |
How is the first approach going to help new users configuring the right executor? I am worried that people will just forget calling The number of functions should be the same between option 1 and 2, thus I'd prefer 2 as it avoids the above footgun because users must choose one of these constructors. |
Regarding builder pattern: All required arguments for a component should be part of the ctor of the builder, with optional parameters then being available as functions. I'd consider the executor a required parameter and thus argue that option 2 also follows the builder pattern correctly. What do you think? |
I am fine with either approach. In the approach I propose, there will be a default executor Self {
local_peer_id,
transport,
behaviour,
pool_config: PoolConfig::new(None),
connection_limits: Default::default(),
} I trust you that this will be inadequate in specific contexts. |
Mark constructors `Swarm::with_X_executor` as deprecated. Move the deprecated functionality to `SwarmBuilder::with_X_executor` Use `SwarmBuilder` throughout. Resolves libp2p#3186. Resolves libp2p#3107. Pull-Request: libp2p#3588.
Description
With the introduction of the
Swarm::with_executor
APIs (#3068), we have a fair bit of duplication in APIs on how to create aSwarm
. Additionally, the currentSwarmBuilder
andSwarm
APIs don't work well with the QUIC and WebRTC transports as those don't go through the typical builder pattern onTransport
and hereby don't construct aStreamMuxerBox
(see #3179).We should streamline the creation of a
Swarm
to work with anyTransport
and be easy to discover. @mxinden suggested to achieve this by forcing users to always go throughSwarmBuilder
.Motivation
Open questions
Are you planning to do it yourself in a pull request?
Maybe.
The text was updated successfully, but these errors were encountered: