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

Load balancer #425

Open
wants to merge 64 commits into
base: client-transport-improvements
Choose a base branch
from
Open

Conversation

Philip-NLnetLabs
Copy link
Member

Load balancer for client transports

@Philip-NLnetLabs Philip-NLnetLabs requested a review from a team October 28, 2024 13:22
@Philip-NLnetLabs Philip-NLnetLabs added the enhancement New feature or request label Oct 28, 2024
Copy link
Contributor

@bal-e bal-e left a comment

Choose a reason for hiding this comment

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

This is a really cool feature! My main concern is the complexity of the most important functions. I'd also like to see more explicit .expect() messages that explain underlying assumptions.

src/net/client/load_balancer.rs Show resolved Hide resolved
src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
// probe upstreams with a queue length of zero. If the queue length
// is non-zero then the upstream recently got work and does not need
// to be probed.
if conn_rt_len > 1 && random::<f64>() < PROBE_P {
Copy link
Contributor

Choose a reason for hiding this comment

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

Prefer to use rand::Rng::gen_bool()?

Copy link
Member Author

@Philip-NLnetLabs Philip-NLnetLabs Nov 13, 2024

Choose a reason for hiding this comment

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

gen_bool is a method. Is it worth first selecting a random number generator to be able to invoke the method? Or is there a shortcut?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the shortest is

use rand::{thread_rng, Rng};
thread_rng().gen_bool(PROBE_P)

Copy link
Member Author

Choose a reason for hiding this comment

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

Is that better than the existing code?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's for you and @bal-e to fight over :)

I don't mind either way. I just wanted to see what it would become.

Copy link
Contributor

Choose a reason for hiding this comment

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

gen_bool() does the same thing you're doing right now, @Philip-NLnetLabs, it's just more explicit. There's no performance change or anything.

Copy link
Member Author

Choose a reason for hiding this comment

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

If gen_bool would be a function, just like random I would agree. But now suddenly we introduce a lot more machinery. It doesn't actually improve the code. But now the reader has to know what thread_rng is and if that is the right choice.

If is even worse, the reader now has to read the description of gen_bool to figure out what is does and when it panics.

I want code to be as simple as possible. And in this case the change seems increase code complexity not decrease it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I disagree with "the reader now has to read the description of gen_bool to figure out what is does and when it panics." gen_bool is a far more descriptive name ("generate boolean") than the current expression. You are implying that readers would understand random better than they would understand gen_bool. What about random's panics? Which random number generator does it use? What range of numbers does it produce? Those are all non-obvious questions that the use of random raises here.

src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
Comment on lines +913 to +935
let mut tmp_conn_rt = conn_rt.clone();

// Remove entries that exceed the QPS limit. Loop
// backward to efficiently remove them.
for i in (0..tmp_conn_rt.len()).rev() {
// Fill-in current queue length.
tmp_conn_rt[i].queue_length = Arc::strong_count(
&conn_stats[i].queue_length_plus_one,
) - 1;
if let Some(max_burst) = conn_stats[i].max_burst {
if conn_stats[i].burst_start.elapsed()
> conn_stats[i].burst_interval
{
conn_stats[i].burst_start = Instant::now();
conn_stats[i].burst = 0;
}
if conn_stats[i].burst > max_burst {
tmp_conn_rt.swap_remove(i);
}
} else {
// No limit.
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of cloning the whole Vec and then filtering out of it, why not use .filter().collect()?

Copy link
Member Author

Choose a reason for hiding this comment

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

That would require two loops: one to update queue_length, burst_start and burst, and another for the filter. Do you think it is worth it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Updating those parameters can be done in forward order too, right? Then you can do them using .map() or .inspect() in the same iterator chain.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm confused. The current loop updates conn_stats. How would that work with map?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, it only updates conn_stats at corresponding indices. You can .zip(&mut conn_stats) or use .enumerate() to get the right index to use.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here's the full code:

let tmp_conn_rt: Vec<_> = conn_rt
    .iter()
    .cloned()
    .zip(&mut conn_stats)
    .filter_map(|(mut conn, stats)| {
        // Fill-in current queue length.
        conn.queue_length = Arc::strong_count(&stats.queue_length_plus_one) - 1;
        if let Some(max_burst) = stats.max_burst {
            if stats.burst_start.elapsed() > stats.burst_interval {
                stats.burst_start = Instant::now();
                stats.burst = 0;
            }
            if stats.burst > max_burst {
                // Filter out the connection.
                return None;
            }
        }
        Some(conn)
    })
    .collect();

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see how this is more readable.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's more efficient and more idiomatic. Readers don't need to consider how entries get filtered using swap_remove(), nor do they have to think about the backward iteration. Instead of indexing conn_stats, we just naturally get the right element from it to work with.

src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
src/net/client/load_balancer.rs Outdated Show resolved Hide resolved
src/net/client/load_balancer.rs Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants