-
Notifications
You must be signed in to change notification settings - Fork 1k
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
feat: add public construction for QueryId
#4508
Conversation
QueryId
QueryId
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.
Thanks for opening the PR!
I don't think we want to add such a constructor because it would potentially break invariants.
For testing Swarm
s, I'd recommend to take a look at: https://github.com/libp2p/rust-libp2p/blob/master/swarm-test/src/lib.rs
We use that across the codebase and it has been working really well for us.
Oh. That looks promising. I'll take a look at that when I get a chance. I'll let you know if that seems like a good alternative. |
The advantage of using this is that your tests use the exact same interface as your prod code, just with a |
Will close this for now! :) |
Is there a way to push events manually using the test swarm? |
Can you elaborate on what exactly you mean? This kind of test design will result in much more stable tests because you are only using the public API of the module. |
For sure! I'm writing code that handles incoming events. There is no concept of connections at this level, just that I'm getting events from somewhere and I want to make sure I handle each event correctly. Since I don't need to worry about connecting with real peers, I've just abstracted away the swarm and created a trait that has the methods I need: pub trait LibP2PSwarm: Send {
fn next_event_fut(
&mut self,
) -> BoxFuture<'_, SwarmEvent<MyBehaviourEvent, Either<Error, Void>>>;
} And I have a fake impl for tests that makes dealing with events a breeze: struct FakeSwarm {
events: Vec<SwarmEvent<MyBehaviourEvent, Either<Error, Void>>>,
}
impl LibP2PSwarm for FakeSwarm {
fn next_event_fut(
&mut self,
) -> BoxFuture<'_, SwarmEvent<MyBehaviourEvent, Either<Error, Void>>> {
Box::pin(async move {
if let Some(event) = self.events.pop() {
return event
} else {
std::future::pending().await
}
})
}
} So I can just say what events are coming in and write tests that check that my code is handling those events correctly.
This seems similar to what I'm doing, in that you're specify the events that you want to trigger. The syntax is confusing me a bit, but I'll admit I'm still learning. But I don't want to worry about managing another swarm--that's out of scope. I will definitely, definitely want to set up some integration tests using the struct RealSwarm {
swarm: Swarm<MyBehaviour>,
}
impl LibP2PSwarm for RealSwarm {
fn next_event_fut(
&mut self,
) -> BoxFuture<'_, SwarmEvent<MyBehaviourEvent, Either<Error, Void>>> {
Box::pin(self.swarm.select_next_some())
}
} Do you see a flaw in my thinking? Or do you know a better way to do this? |
I see what you are trying to get at but this is going to be quite hard to achieve I am afraid. Purposely, several of the events that we emit have crate-private constructors which allows us to attach invariants to them. We can then be sure that no-one apart from us can construct a particular ID etc. It also minimizes the public API we have to commit to and can treat more things as implementation details. I'd recommend you do one or both of the following:
I'd advise against directly abstracting rust-libp2p/examples/chat-example/src/main.rs Lines 119 to 124 in f5e644d
Not sure how new you are to Rust but in my experience, mocking data doesn't scale very well. Rust more or less gives you "integration tests" for free through its type-system and exhaustive pattern matching. If it compiles without warnings, you are probably fine on that front. Thus, I'd focus on tests of extremely small components that include critical decision-logic. You can probably extract those out into free functions. Lastly, I'd top it off with some larger e2e tests, possible still based on a memory-transport if you also can run the rest of your app completely within a process. In general, Rust's type system results in really strong API contracts. But that also means that tiny changes can cause a ripple effect of compile errors through your codebase, even when all you wanted to do is make a small refactoring. My personal conclusion from this is that these kind of "mid-size" tests often hurt more than they help because they rely on APIs that change frequently which makes refactorings really painful because now you suddenly have all these tests that you need to fix too. Instead, you should figure out which APIs in your system are the least likely to change. Those you should write your tests against. |
A piece of advice regarding traits: Don't treat them as interfaces. Forget everything you've learned about OOP. Traits should abstract over functionality. My rule of thumb is: If you don't have production code that is generic over your trait (i.e. Just my 2c :) PS: I recently wrote a TURN server. Roughly 5000 LoC: https://github.com/firezone/firezone/tree/main/rust/relay Guess how many traits it has? Just 7 but two are in the process of being upstream to a library and three are extension traits. The remaining two are legit traits that are being used as abstractions, see https://github.com/firezone/firezone/blob/10faffc4dbbc8fef7b2573f15a34357f1236569a/rust/relay/src/server.rs#L731-L735.
|
I'm hitting the hay so I'm gonna have to digest everything you've written later, but thanks for all your input! Just a little context, I'm not new to Rust; I've been using it for almost five years for work. The places I've worked have emphasized clean architecture and test-driven development with Rust. I like to approach designs with hexagonal architecture in mind (https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)?wprov=sfti1). All this to say, I know how I want to design my code but I'm not sure if libp2p was designed in a way that will make it easy. I'd prefer a library expose a trait of the interface I need to depend on for IO so that I can fake it as I please--I identified that interface as the API of the swarm since it is what is used in the event loops. I've only seen concrete swarms used in any of the example code though, and I think I can see why based on how the swarm is constructed. That's why I went for my own trait. This is for a personal project where I've abstracted the storage of the system away and this libp2p event handling is in an "adapter" for the storage. However a lot of my findings are gonna spill over into my work where we use libp2p. That code is overly coupled to libp2p and it's causing problems with maintainability. This personal project is kinda an exploration of solutions I hope to implement at work. |
I am a big fan of that. So much that I actually wrote a blog post on it several years ago: https://blog.eizinger.io/5835/rust-s-custom-derives-in-a-hexagonal-architecture-incompatible-ideas. Disclaimer: It is 4 years old and I don't hold the exact same believes any more. There is definitely value in a ports-and-adapters design. If I were to implement one today, I think it'd go down the path of wrapping all of networking in a module (i.e. the eventloop described above) and treating that as the interface you are abstracting away. This is similar to how you probably have an application-specific interface for your storage and don't directly code against SQL in your business logic, even though SQL is technically already an abstraction layer where you could swap a production Postgres for an in-memory SQlite for testing. Really, it comes down to which components you want to be able to swap out and I think, directly swapping out
Would love to learn more about the problems you are facing! Are you aware of our open-maintainers call, e.g. https://github.com/libp2p/rust-libp2p/discussions?discussions_q=open+maintainers+call+? The next one scheduled for September 26th. Also happy to hop on a call before that if you fancy.
I am guessing you are talking about https://github.com/FuelLabs/fuel-core? It would be great if you could update to the latest version 😇 |
This is rather difficult with libp2p because we manage connections under the hood and for example spread them across different tasks of your runtime to keep the latency of the behaviour state machine down. Additionally, given the encryption and multiplexing that we are doing, I don't see how you would actually be able to meaningfully fake that IO. There is a universe in which libp2p is implemented in a SANS-IO way and you need to do all the IO yourself. That scales better than having interfaces, as interfaces are always a contract you are locked into compared to a state machine that you need to drive. For example, it is why I think it is worth exploring getting rid of I don't think using |
Currently, I'm just copying the behavior from the distributed key share example, so I'm not as worried about the testing the behavior as I am testing my code that interacts with it--that's the interface I want to fake.
Yes. I do already have a event loop which takes input from the |
That's right! I've only been on the team for a couple months, so I'm still getting familiar with all the code. Refactoring isn't the priority right now, so I can't say that it will be super soon, but I'd love to jump on a call and discuss things once I done some serious spiking--it's possible it won't be major. In the mean time I'm settling with incremental changes to push things in the right direction and to see what is fragile 👍 and working with libp2p on personal projects to make sure I have a sound grasp. |
Good! I only mentioned it because it is a not-so-often-considered option of using What I am getting at is: If you can push certain functionality into a plugin, it is easier to (unit)-test that compared to adding more and more things on top. The only thing that plugins can't do is send arbitrary messages to each other. They likely will never be able to because it would cause circular dependencies between them.
Yes. Do note that some operations might fail if we don't have any active connections.
Sounds good! Would be great to have you on the call :) |
All sounds good! My tests are now at parity with where they were before and I'm feeling optimistic about moving forward. I'm going to have a lot more confidence that the code will do what I expect in the wild now that the tests are using more true I'm sure I'll have more questions moving forward, but I'll open an issue or discussion rather than having another back-and-forth in some defunct PR :P Thanks for your help and thanks for being a good maintainer ❤️ |
Description
Adding a way for
QueryId
to be constructed by the library consumerNotes & open questions
The reason I need this is I am creating an abstraction of Libp2p in my code so that I can have a
Swarm
trait rather than depending on the concrete swarm directly. This allows me to fake swarm behavior in my tests. I need to be able to generateQueryId
s in these fakes :)I don't believe these changes require documentation or tests, but happy to add anything the maintainers would like :)
Change checklist