Skip to content
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

[Sharding 4] Base testing support for sharding #1369

Merged
merged 7 commits into from
Oct 29, 2024

Conversation

cberkhoff
Copy link
Collaborator

This is the fourth of a series of pull requests (PR) to enable sharding on IPA. See #1364

This change doesn't meaningfully change any tests or the operation of IPA. This is just adding abstractions that are going to be useful later.

Biggest change here is in net/test.rs module, which was enhanced to create Sharded infrastructure. This will become more useful for the sharded shuffle test, but releasing early allows reviewers to check for the intended architecture and its usage.

Copy link

codecov bot commented Oct 24, 2024

Codecov Report

Attention: Patch coverage is 98.90411% with 4 lines in your changes missing coverage. Please review.

Project coverage is 93.66%. Comparing base (933a902) to head (29258c5).
Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
ipa-core/src/net/test.rs 98.46% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1369      +/-   ##
==========================================
+ Coverage   93.64%   93.66%   +0.01%     
==========================================
  Files         223      223              
  Lines       37112    37376     +264     
==========================================
+ Hits        34755    35008     +253     
- Misses       2357     2368      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


impl TestNetwork<Shard> {
#[must_use]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

/// 3 mpc ports.
ports_by_ring: Vec<Ports<Vec<u16>>>,
/// Describes the number of shards per helper
sharding_factor: usize,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I see factor I think about the multipler. shard_count is easy and straightforward

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was intended because you're getting 3x hosts for the factor.

If you still think shard_count is easier I can do that, but this factor affects all 3 helpers.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then /// Describes the number of shards per helper confuses me even more

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? it says per helper aka 3x

#[must_use]
pub fn with_open_ports() -> Self {
pub fn with_http_and_default_test_ports() -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the functionality to request shard ports from the Kernel, is it planned? Static ports are constant source of pain for tests and I don't think we should go with static. It is fine to test configs, but all unit tests should run with random ports assigned at runtime

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting ports from the kernel is part of the default method.

In the past we used to have both, with_open_ports and default, which did the same thing. I thought less code is better.

optional_ports: Option<Vec<u16>>,
ring_index: usize,
) -> TestServers {
let mut sockets = vec![None, None, None];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just Vec::with_capacity(3)?

});
let (scheme, certs) = if self.disable_https {
("http", [None, None, None])
// Ports will always be defined after this. Socks only if there were no ports set.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this block of code and comment to it. Why we use all ports available to build servers in HTTPS mode and only 3 for HTTP?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to what part you're referring...

But this function creates 3 servers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After line 270 all the port situation will be set. Either you gave it as param or we asked the os.

Not sure to what part you're referring when you say "Why we use all ports available to build servers"... But this function creates 3 servers.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confusion comes from either using optional_ports or synthesizing ports inside

optional_ports: Option<Vec<u16>> which can be arbitrary size according to this function (it does not enforce size to be 3, if it should, then it would be better to take an array). So ports may have variable size

}

/// Defines a test network with a single MPC ring (3 helpers).
fn build_ring_network(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this change TestConfig looks a lot more complicated that it was before. I think it will benefit from some refactoring to split MPC and shard network initializations

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, Test config holds now the entire sharded network. But on the other hand, having everything described on this single object makes it easy to operate on the network as a whole which is the use case (check the transport.rs test). Separating this into 2 seems like it will only add complexity on the caller side.

Can you be more explicit on how you want to split the network init?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't have to make callers suffer. Providing a single entry point for them to construct the config is reasonable. Doing everything inside one struct after that is not necessary - power through composition

Comment on lines 357 to 360
let rings: Vec<TestNetwork<Helper>> = (0..self.sharding_factor)
.map(|s| {
let ring_ports = self.ports_by_ring.get(s).map(|p| p.ring.clone());
let shards_ports = self.ports_by_ring.get(s).map(|p| p.shards.clone());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let rings: Vec<TestNetwork<Helper>> = (0..self.sharding_factor)
.map(|s| {
let ring_ports = self.ports_by_ring.get(s).map(|p| p.ring.clone());
let shards_ports = self.ports_by_ring.get(s).map(|p| p.shards.clone());
let r = zip(ring_ports, shard_ports).map(|(r_port, s_port)| { ... })

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are cases where ports_by_ring won't be set though.

}

#[must_use]
pub fn create_ids(disable_https: bool, id: HelperIdentity, ix: ShardIndex) -> ClientIdentities {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be a constructor on ClientIdentities?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. The conditional on disable_https had me on the fence

let (mut certificate, mut private_key) = get_test_certificate_and_key(id);
ClientIdentity::from_pkcs8(&mut certificate, &mut private_key).unwrap()
pub fn get_client_test_identity(id: HelperIdentity, ix: ShardIndex) -> ClientIdentities {
let pos = ix.as_index() * 3 + id.as_index();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that deserves a comment and a common function to compute the absolute offset as I believe it is used in more than one place

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

/// The network itself won't be excersized as that's tested elsewhere.
#[test]
fn create_4_shard_http_network() {
let ports: Vec<Ports<Vec<u16>>> = vec![
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to test that overlapping ports are rejected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can add that

Copy link
Collaborator

@akoshelev akoshelev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't store a TcpListener inside a configuration struct, so I'd like this to be fixed before pushing it, but other than that lgtm

/// Contains the ports
pub config: ServerConfig,
/// Sockets are created if no port was specified.
pub socket: Option<TcpListener>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as much as I like the idea of holding onto TcpListener until the moment when server is spawned - minimizes the chances of Kernel re-using that port in between - I don't think the approach of storing TcpListener in the configuration struct is appropriate here. TcpListener does not describe the configuration, it actively uses a system resource (socket)

Maybe we can get away with using file descriptors, but honestly I wouldn't bother as I've never seen an issue with bind/release.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a chain of dependencies; PeerConfig requires a port, etc.
Changing this won't be simple. I can add a todo.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be clear - I am not suggesting to get rid of the port here. If port is not provided by the caller, you can do the previous workaround (ask kernel for a port and drop the listener after that)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I think that might be problematic with many tests running at the same time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kernel is good not releasing the port right away

Comment on lines 378 to 379
.map(|r| r.shards[id.as_index()])
.map(Some)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(|r| r.shards[id.as_index()])
.map(Some)
.map(|r| Some(r.shards[id.as_index()]))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, do you prefer larger blocks of code inside a map function instead of more map calls?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is always a balance, cluttered map calls are bad for readability, more map calls result in more complex types used and more work for compiler to unwind them

}

/// Get all the MPC ports in a ring specified by the shard index.
fn get_ports_for_shard_index(&self, ix: ShardIndex) -> Vec<Option<u16>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from the ergonomic perspective, a better type to return would be Option<Vec<u16>> here

@@ -9,46 +9,171 @@

#![allow(clippy::missing_panics_doc)]
use std::{
array,
collections::HashSet,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider splitting this file into multiple - it grew too big and Github now hides all the changes for it by default

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking on separating the TestServer stuff

@cberkhoff cberkhoff merged commit 4d50a64 into private-attribution:main Oct 29, 2024
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants