Skip to content

execution_profile: whitelist/blacklist filtering #291

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

muzarski
Copy link
Collaborator

@muzarski muzarski commented Apr 29, 2025

Fixes: #243

This PR introduces whitelist/blacklist host filtering to the driver.

Filtering rules

There are currently 4 filtering rules (we could introduce 2 more in the future - rack filtering):

  • ip whitelist
  • ip blacklist
  • dc whitelist
  • dc blacklist

The logic for checking if a host is accepted is following (in the specified order):

  1. if ip whitelist is non-empty, and if it does not contain host's ip -> reject
  2. if ip blacklist is non-empty, and if it contains host's ip -> reject
  3. if dc whitelist is non-empty, and if it does not contain host's dc -> reject
  4. if dc blacklist is non-empty, and if it contains host's dc -> reject

HostFilter

Since all of the execution profiles are defined before session creation (in other words: user cannot define new profiles after session was created and connected), cpp-driver (and so, cpp-rust-driver as well) can see which hosts are disabled by all execution profiles.

If there is a host which is rejected by all execution profiles (including the default one), the driver does not open the connection to such host. This is exactly what we do in this PR - after collecting all of the filtering rules of each profile, we compute the filtering rules for our custom HostFilter implementor - namely CassHostFilter. This is done by computing the unions of whitelists and intersection of blacklists.

Vec or HashSet

For now, I decided to use Vec to represent the set of items in the whitelists/blacklists. I'd say that we do not expect them to contain a lot of items, so I think Vec is more efficient than HashSet in this specific case. One could argue that this code is not critical (and I agree) - this is a configuration phase. I'm open to suggestions.

Integration tests

ExecutionProfile suite

I have not enabled any yet - they require cass_future_coordinator, which is used to check the ip of host the request was routed to. TBH, I think this is a blocker for merging - I prefer to wait for the cass_future_coordinator, and enable the corresponding tests (and maybe implement some additional tests on my own). I still believe that this PR is ready for review, though.

HostFilter suite

This is the suite I implemented myself - currently there are two tests that reject all nodes using execution profiles - in result, session does not have any connections to route the requests to.

DisconnectedNullStringApiArgs suite

Enabled remaining 4 test cases from this suite - they check whether driver behaves as expected when null string is provided to filtering config methods.

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • PR description sums up the changes and reasons why they should be introduced.
  • I have implemented Rust unit tests for the features/changes introduced.
  • I have enabled appropriate tests in .github/workflows/build.yml in gtest_filter.
  • I have enabled appropriate tests in .github/workflows/cassandra.yml in gtest_filter.

@muzarski muzarski changed the title Execution profile filtering execution_profile: whitelist/blacklist filtering Apr 29, 2025
@muzarski muzarski self-assigned this Apr 29, 2025
@muzarski muzarski added the P1 P1 priority item - very important label Apr 29, 2025
@muzarski muzarski added this to the 0.5 milestone Apr 29, 2025
@muzarski muzarski force-pushed the execution-profile-filtering branch from 7608c10 to 5e8b9a7 Compare April 29, 2025 14:16
@muzarski muzarski marked this pull request as draft April 29, 2025 16:04
@muzarski muzarski force-pushed the execution-profile-filtering branch 5 times, most recently from 8f3ced7 to d0c2e66 Compare April 30, 2025 11:45
@muzarski muzarski marked this pull request as ready for review April 30, 2025 11:47
@muzarski muzarski requested review from wprzytula and Lorak-mmk April 30, 2025 11:47
Comment on lines 294 to 283

/// Returns the union of all vectors (sets).
/// If the union is empty, it returns None.
fn nonempty_union<'a, T>(iter: impl Iterator<Item = &'a Vec<T>>) -> Option<Vec<T>>
where
T: Clone + Ord + 'a,
{
let mut union = Vec::new();

for values in iter {
for v in values {
if !union.contains(v) {
union.push(v.clone());
}
}
}

(!union.is_empty()).then_some(union)
}

/// Returns the intersection of all vectors (sets).
/// If the intersection is empty, it returns None.
fn nonempty_intersection<'a, T>(mut iter: impl Iterator<Item = &'a Vec<T>>) -> Option<Vec<T>>
where
T: Clone + PartialEq + 'a,
{
// Get the first set (initial intersection).
let mut intersection = iter.next().cloned();

// Remove the items from the intersection that are not present in the other sets.
if let Some(intersection) = intersection.as_mut() {
for set in iter {
intersection.retain(|item| set.contains(item));
}
}

// If the intersection is empty, return None.
intersection.filter(|v| !v.is_empty())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Commit: lbp: introduce utility methods for set operations

I think that with host filtering the performance of Vec vs HashSet is not relevant (this is not even close to hot path). What we should look at is readability. Wouldn't methods present in HashSet / BTreeSet help with implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On a high level, there is no HashSet API that would allow us to implement these as a "one-liners". The logic for intersection would remain the same. However, the logic for union could be simplified - we can convert each set to iterator, then flatten and collect to HashSet. Currently, we need a loop, so we don't insert the duplicates.

I have nothing against transitioning to HashSet - as you said - this is not close to hot path.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't some combination of fold (or similar Iterator methods) and union / intersection HashSet methods allow to express both of those more cleanly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

union and intersection methods take two HashSets and return an iterator. I don't see a neat way to generalize it and reuse these methods to implement union/intersection for multiple sets (without needless collect()ing in fold). I think that the retain method is clean and efficient 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.

Ok

@muzarski
Copy link
Collaborator Author

muzarski commented May 5, 2025

I have already found a bug in my HostFilter filtering rules computation. Consider following scenario:
We have two nodes: "ip1" and "ip2". Let's say we have two execution profiles, one with the whitelist being "ip1", the other with empty (disabled) whitelist. In result, the HostFilter whitelist will be a union of these -> "ip1". In result, we filter out the "ip2" node - even though it is accepted by at least one execution profile (the one with empty whitelist).

I'll fix it - the nonempty_union should return None, if at least one of the provided sets is empty (i.e. one of the whitelist rules is disabled). This proves that we need the tests before merging this.

Note: the nonempty_intersection looks good in such scenario, because if at least one of the blacklists is empty, the resulting HostFilter blacklist will be empty (disabled) as well.

@muzarski muzarski force-pushed the execution-profile-filtering branch from d0c2e66 to 5caf82b Compare May 5, 2025 06:03
@muzarski
Copy link
Collaborator Author

muzarski commented May 5, 2025

Rebased on master.

@muzarski muzarski force-pushed the execution-profile-filtering branch from 5caf82b to 3ac0535 Compare May 5, 2025 07:17
@muzarski
Copy link
Collaborator Author

muzarski commented May 5, 2025

v2:

  • addressed @Lorak-mmk comments (haven't transitioned to HashSet yet - still to be discussed)
  • fixed the bug I mentioned above
  • introduced HostFilterTest suite and implemented two test cases where we disable all nodes (driver has no connections to route the queries).

@muzarski muzarski requested a review from Lorak-mmk May 5, 2025 07:19
@muzarski muzarski force-pushed the execution-profile-filtering branch from 3ac0535 to f5a7f9d Compare May 5, 2025 08:16
@muzarski
Copy link
Collaborator Author

muzarski commented May 5, 2025

v2.1: Previously I implemented new tests, but forgot to enable them..

Copy link
Collaborator

@Lorak-mmk Lorak-mmk left a comment

Choose a reason for hiding this comment

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

Looks good to me. I don't really care if we use Vec or HashSet, so I'm approving now. If you decide to change the container, then I'll re-approve.

muzarski added 5 commits May 5, 2025 16:01
This PR introduces a lot of stuff related to load balancing. It does
not make much sense to have it all in cluster.rs.
It will be used to configure filtering via methods such as
cass_cluster_set_whitelist_filtering[_n].
The semantics are exactly the same as in `cass_cluster_set_contact_points`.
This is why we can reuse the `update_comma_delimited_list` method.

Note: Cluster methods for some reason do not return `CassError`, while
execution profile methods do. This is why the error ignored in cluster case.
At first I thought that the logic differs between these two, but no - execution
profile methods always return CASS_OK (because cpp-driver does not do any
pointer validation...).
muzarski added 8 commits May 5, 2025 16:07
Nothing special here, just introducing a new struct with a simple
HostFilter trait implementation.

More interesting thing is how we construct the filtering rules for the
HostFilter - this will be handled later in this PR. This commit
is introduced early to reduce the noise during review later.
Motivation is going to be further explained in the later commit,
where I explain how cpp-driver decides to which hosts it opens
the connections. In short: if **all** execution profiles ignore the host, the driver
does not open the connection to it.

Knowing how filtering rules are applied, I noticed that we can implement
the filtering rules for host filter by computing the unions and intersections
of whitelists and blacklists respectively.
Note: Empty list means that this filtering rule is disabled.
In cpp-driver, the following rule is upheld: if a host is rejected
by **all** policies, the connection to that host is not opened at all.

We can achieve this by:
- taking the union of all whitelists (per hosts and per dcs)
- taking the intersection of all blacklists (per hosts and per dcs)

Now, if a host is not in the union of whitelists, it is rejected.
If a host is in the intersection of blacklists, it is rejected.

Note: if the execution profile does not have a base LBP defined
(one of: round-robin, dc-aware, rack-aware), all of the extensions
are ignored - including the filtering rules. This is achieved by filtering
out such profiles in CassCluster::build_host_filter.
The filtering is now implemented. Unfortunately, we cannot enable
any integration tests yet. They require cass_future_coordinator.
As I mentioned before - this error is returned when we for some reason
cannot route the request to one of the hosts. I forgot to address this earlier
(there was no test case for empty plan). I'll introduce one later in this PR.
Added two test cases where we disable all nodes using execution profile
filtering. In result, the driver should not open any connections the
requests can be routed to.
They can now be enabled since filtering config is implemented.
@muzarski muzarski force-pushed the execution-profile-filtering branch from f5a7f9d to 5aced6c Compare May 5, 2025 14:08
@muzarski
Copy link
Collaborator Author

muzarski commented May 5, 2025

v2.2:

  • Rebased on master
  • Implemented set_[whitelist/blacklist]_filtering methods using update_comma_delimited_list utility method introduced here. I removed the set_filtering helper function which was in my first version of this PR - it's the same as update_comma_delimited_list.
  • Enabled remaining DisconnectedNullStringApiArgs tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P1 P1 priority item - very important
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement whitelist/blacklist filtering
2 participants