The building blocks of rust-libp2p
#3603
Replies: 2 comments 7 replies
-
Thanks for the clear presentation! I fully agree with your assumption that the split into long-lived and per-connection pieces is correct. One way to think about what to do with streams on a connection is that there are two domains: the network is streaming octets (one after the other) while the host is synchronous and structural (we have everything present at the same time, to inspect it at our leisure). With this definition, the network boundary is where the stream is consumed (which may mean turning it into a structure or sending it elsewhere again). Now the basic classification of use-cases obviously permits two options:
In the former case, a received message may be sent to an interested actor inside the libp2p-based application, or it may be handled by the behaviour (either in In the latter case, the consumer of an incoming stream or the producer of an outgoing stream lives outside the Swarm, so the behaviour will have APIs that expose channels for sending and receiving the buckets of bytes. So to me it seams that we might want to expose the following APIs for behaviour authors:
Options 2 and 3 can obviously be combined, people may simply drop the stream sending channel. The experience for end user code would then be: “I want to initiate a session of protocol X with PeerId P and here are the parameters, let me know when it’s done.” More complex cases could easily have multiple steps where intermediate results are provided to the caller who is then expected to send back some decision on how to continue. The streaming data use-case is also covered by simply making the messages exchanged with code outside the Swarm be buckets of bytes (plus metadata as applicable). This would be a totally awesome API and dev experience, I think. |
Beta Was this translation helpful? Give feedback.
-
If you try to port the chat example to use stream::Merge instead of |
Beta Was this translation helpful? Give feedback.
-
In this post, I want to summarize and outline a vision I have been forming around what the internal design of rust-libp2p should be. It is based on many experience reports and from working with the library for the past couple of years.
Initially, it was motivated by coming up with a design for what I call "custom protocols" but I soon realized that what I actually want to document are the layers and components I see that eventually compose to the various features we have today.
To begin with, we need to look at the constraints of what we are working with:
Swarm
has a set of behaviours. Those can be seen as vertical slices of functionality. Vertical because they don't "see" each other but can operate across the entire stack, i.e. take input from the user and manage individual connections.NetworkBehaviour
operates as a single instance. Each connection is given aConnectionHandler
. Those again are vertically composed. EachNetworkBehaviour
produces oneConnectionHandler
for each connection. A multiplexing component allows thoseConnectionHandler
s to operate across the same physical connection.NetworkBehaviour
s andConnectionHandler
s communicate with each other via message passing.These abstractions are guided by the physical underpinnings of our domain: networking. In my opinion, it would be a mistake to try and hide some of these details. For example, spawning a new
ConnectionHandler
per connection makes it easy for users to incorporate the lifecycle of a connection into their protocol. There are some simplifications to be made to these interfaces, for example:ConnectionHandler
trait by removing as many associated types as possible #2863ConnectionHandler::{In,Out}boundOpenInfo
#3268ConnectionHandler::Error
#3591However, none of these question the base abstraction of splitting protocols into a long-lived component (
NetworkBehavior
) and one per physical connection (ConnectionHandler
). The following vision is based on the idea that these abstractions are correct.Not all protocols need everything provided by these abstractions. That is where our users perceive a lot of boilerplate. If all I need is to send a request and receive a response, I don't really care which connection it is sent over. Having to pass a message down across 2 layers and the response back up is unnecessary. We have
libp2p-request-response
but that implements a very specific usecase. If what you are doing doesn't fit this usecase, you are presented with nothing again.What we need is a set of components that gradually compose to the functionality we want, minimising boilerplate at each step of the way. Thus, if whatever component we provide does not fit the user's usecase, they can go and replace it with something else.
For the benefit of these usecases and also to reduce code duplication within our own protocols, I see the following stack of components:
asynchronous-codec
to go from streams to messages: AConnectionHandler
presents us with a stream that implementsAsyncRead
andAsyncWrite
. For most protocols, we will want to decode these into messages. We already have theasynchronous-codec
crate which helps us with this. It is however purely declarative. In other words, using the crate doesn't actually give you messages, it gives you aStream
and aSink
that will yield messages when polled.asynchronous-codec
with message patterns in Prototype message patterns:RecvSend
,SendRecv
andSend
mxinden/asynchronous-codec#5.TimedStreams
to bound IO: Operations need to have timeouts, otherwise bad things will happen, like hostile peers deliberately hogging our resources. I envisionTimedStreams
to be a wrapper aroundSelectAll
that applies a timeout to each item added to the list. In case the stream (which will be a "message pattern" in the majority of cases) does not finish within the specified timeout, it will automatically be aborted (by dropping it).Notice how:
TimedStreams
is entirely optional composition step and not strictly mandatoryNow we can go from streams to sending messages back and forth but so far, we haven't dealt with
ConnectionHandler
andNetworkBehaviour
yet. The above components will reduce some boilerplate (no more manual state machines) but to actually have a working protocol, users will still need to implement aNetworkBehaviour
and aConnectionHandler
incl. the message passing between them which isn't exactly fun.Here is where we have several choices. Somehow, we need to give users access to streams and "cut" through the layers provided by
NetworkBehaviour
andConnectionHandler
. Several proposals have been made in the past:FromFn
ConnectionHandler
#2852This is where I am still undecided. Essentially, it boils down to: Do we want "streams" to escape a
ConnectionHandler
(and thus the task they are running on) or not?If we are okay with this, then something like the proposed
libp2p-bare-stream
is the way to go. It gives users access to streams and they can do with them what they want, either compose them with any of the abstractions above or do something entirely different.However, I think there is also a point to be made about containing networking logic within a
ConnectionHandler
. In that case, something like aFromFn
ConnectionHandler
is much more appropriate. It is easy to extend this to aNetworkBehaviour
if we want to where users provide the callback directly and don't have to implement any trait themselves.Should we do both and let users decide? That does feel a bit lazy too. It seems like we should use our knowledge of the library to provide a "here is a good way of doing things" approach and not give users multiple choices, essentially forcing them again to get into the details to understand the trade-offs.
Beta Was this translation helpful? Give feedback.
All reactions