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

Pick connections based on batch first statement's shard #508

Merged
merged 1 commit into from
Sep 30, 2022

Conversation

Ten0
Copy link
Contributor

@Ten0 Ten0 commented Aug 14, 2022

Fixes: #448
Helps #468

To achieve this, it was useful to rework the BatchValues trait.
With this rework, it's also possible to pretty easily provide cloneable Iterators over ValueList as BatchValues (through BatchValuesFromIterator) - so #499 may get closed if this gets merged (or this PR will require update if #499 gets merged before).

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • I added relevant tests for new features and bug fixes.
  • All commits compile, pass static checks and pass test.
  • PR description sums up the changes and reasons why they should be introduced.
  • I added appropriate Fixes: annotations to PR description.

@@ -1315,7 +1344,7 @@ impl Session {
.await
}

fn calculate_token(
pub fn calculate_token(
Copy link
Contributor Author

@Ten0 Ten0 Aug 14, 2022

Choose a reason for hiding this comment

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

I'm not sure if this (plus the added Hash for Token) is enough to resolve #468, insight welcome.

Copy link
Contributor Author

@Ten0 Ten0 Aug 14, 2022

Choose a reason for hiding this comment

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

Hmm looks like it's not, as this specifically asks for shard, and not only token (which looks like it would improve batching abilities).
This is a good first step though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's unclear for me how this should be done and likely would conflict with #491.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't worry about conflicts with #491, it's a larger effort and we'll make sure that it works correctly with all previously added features. As for #468, our driver is internally capable of computing which shard owns a particular token (if all the information is properly fetched and available), take a look at this test case of a Sharder:

#[cfg(test)]
mod tests {
use super::Token;
use super::{ShardCount, Sharder};
use std::collections::HashSet;
#[test]
fn test_shard_of() {
/* Test values taken from the gocql driver. */
let sharder = Sharder::new(ShardCount::new(4).unwrap(), 12);
assert_eq!(
sharder.shard_of(Token {
value: -9219783007514621794
}),
3
);
assert_eq!(
sharder.shard_of(Token {
value: 9222582454147032830
}),
3
);
}

So if you go deep enough, it's possible to expose this sharding information publicly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sharder appears to be a property of a Node, so it looks like I'd have to go through building the query plan, getting the corresponding node (what if there are several nodes output by the plan? just take the first one?) then get the sharder for the node and eventually return something like a (node, shard) pair, but I don't know what we could use as node identifier, in order to allow users to group by that.

It looks like providing an interface for computing the token is a good first step still, as it will allow grouping at least by partition if not by shard, and exposing the shard interface can be done in a later PR.
As it's unlikely I'll have time for implementing the shard interface anytime soon, I'd suggest moving forward with this without this additional feature.

@Ten0 Ten0 changed the title Pick connections based on first statement's shard Pick connections based on batch first statement's shard Aug 14, 2022
@Ten0 Ten0 force-pushed the shard_aware_batch branch 2 times, most recently from b7c82e5 to 06a5897 Compare August 14, 2022 23:11
scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
@Ten0 Ten0 force-pushed the shard_aware_batch branch 2 times, most recently from c19d684 to 4fb5ca9 Compare August 15, 2022 15:48
@Ten0 Ten0 mentioned this pull request Aug 16, 2022
6 tasks
@psarna psarna requested a review from piodul August 17, 2022 08:36
@cvybhu
Copy link
Contributor

cvybhu commented Aug 17, 2022

Alright I had a go at the patch. My initial reaction was that it's way too complicated, but after thinking this through I think I agree that this is the way. Here is a summary to gather thoughts and make reviewing easier for others.

We want a few things from the new BatchValues trait:

  1. We want to be able to write all of them to the request BufMut
  2. We want to be able to get SerializedValues for one of the statements inside the batch in order to calculate the token. Being able to get them only for the first ValueList would work for now, but maybe in the future we will improve the heuristic - it would be good to be able to get them for any chosen statement.
  3. Writing to request and getting the values must be done at separate times, because calculating token happens in Session::batch, while request serialization is much deeper. That's why any iterators of ValueList must be Clone.
  4. We want to get the number of BatchValues in Session::batch to validate that the user passed values for each statement inside the batch. For all common implementations (tuples, slices, vecs, etc) this is easy to get. For iterators they have to be an ExactSizeIterator or we could do .clone().count()
  5. BatchValues should be creatable from an iterator of ValueLists to avoid collecting it and needlessly allocating

Seeing these requirements one could attempt to write:

trait BatchValues {
    type BatchValuesIter: BatchValuesIterator;

    fn iter(&self) -> Self::BatchValuesIter;
    fn len(&self) -> usize;
}

trait BatchValuesIterator: Clone {
    fn write_next_to_request (&mut self,buf:  &mut impl BufMut) -> Option<()>;
    fn next_serialized(&mut self) -> Option<SerializedResult<'_>>;
}

But there is a problem when the type BatchValuesIter has a refernce and requires a lifetime.
With GATs we could write:

trait BatchValues {
    type BatchValuesIter<'a>: BatchValuesIterator<'a>;

    fn iter<'a>(&'a self) -> Self::BatchValuesIter<'a>;
}

But because there are no GATs we end up with the workaround (which is really cool btw).

Looks very nice, I will read the rest and submit a review.

@Ten0
Copy link
Contributor Author

Ten0 commented Aug 17, 2022

Alright I had a go at the patch. My initial reaction was that it's way too complicated, but after thinking this through I think I agree that this is the way.

That was exactly my thought process, thanks a lot for the detailed write-up! 😃

which is really cool btw

Yeah at this point it has saved us more than once within Diesel 😊

Copy link
Contributor

@cvybhu cvybhu 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, I only left some style comments.

Style is highly subjective, but let's try to keep it consistent with the rest of the code.

scylla-cql/src/frame/value_tests.rs Show resolved Hide resolved
scylla/src/routing.rs Outdated Show resolved Hide resolved
scylla/src/transport/session.rs Outdated Show resolved Hide resolved
scylla-cql/src/frame/request/batch.rs Outdated Show resolved Hide resolved
scylla-cql/src/frame/value.rs Show resolved Hide resolved
scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
@cvybhu
Copy link
Contributor

cvybhu commented Aug 19, 2022

I'm worried that our tests might not test all code paths here.
Everywhere I look the first statement in a batch is shard aware.

In cases where the first PreparedStatement is not shard aware calculate_token will fail, This failure seems to be handled gracefully but still it would be nice to actually test it.

Ten0 added a commit to Ten0/scylla-rust-driver that referenced this pull request Aug 19, 2022
Especially useful in conjunction with batching, even more so with
shard-aware batching (scylladb#508)
@Ten0
Copy link
Contributor Author

Ten0 commented Aug 19, 2022

In cases where the first PreparedStatement is not shard aware calculate_token will fail

Returns Ok(None), I'm not sure whether to even call that a fail ^^

if !prepared.is_token_aware() {
return Ok(None);
}

Anyway, I'm just handling it the exact same way as is done in execute_paged:

token: self.calculate_token(ps, first_serialized_value)?,

let token = self.calculate_token(prepared, &serialized_values)?;
let statement_info = Statement {
token,

@cvybhu
Copy link
Contributor

cvybhu commented Aug 23, 2022

There is a merge conflict with scylla/src/transport/session.rs , please rebase.

@cvybhu
Copy link
Contributor

cvybhu commented Aug 23, 2022

Also could you squash these two commits together, both of them contain changes related to review comments but only one is named like it does.

@cvybhu
Copy link
Contributor

cvybhu commented Aug 23, 2022

I'm almost ready to merge this, but it would be good for someone else to take a look as well, @piodul @psarna .

Copy link
Contributor

@psarna psarna left a comment

Choose a reason for hiding this comment

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

@Ten0 could you rebase and get rid of the branch conflicts? @cvybhu looks good to me, I think I can follow the logic behind GAT workaround, and I see that you reviewed the code thoroughly, so feel free to merge once rebased (and the minor clippy issue is fixed)

scylla-cql/src/frame/value.rs Outdated Show resolved Hide resolved
@cvybhu
Copy link
Contributor

cvybhu commented Sep 9, 2022

Apart from rebasing, one more thing I would love to see before merging is #508 (comment)
The new interface gives us an iterator, but we don't test that after running out of items it returns None.
It isn't strictly required, but it would make the test complete and should be quick to add.

I will be on vacation next week, so I won't be able to merge it then.

To achieve this, it was useful to rework the BatchValues trait.
With this rework, it's also possible to pretty easily provide cloneable `Iterator`s over `ValueList` as `BatchValues` (through `BatchValuesFromIterator`)
Copy link
Contributor

@cvybhu cvybhu left a comment

Choose a reason for hiding this comment

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

The None case could be added to the other cases as well, but let's avoid spending extra time on the details.

@cvybhu cvybhu merged commit 7c877f2 into scylladb:main Sep 30, 2022
@Ten0 Ten0 deleted the shard_aware_batch branch October 1, 2022 22:05
Ten0 added a commit to Ten0/scylla-rust-driver that referenced this pull request Mar 11, 2023
Helps (or arguably fixes) scylladb#468

For smarter batch constitution (following up on scylladb#448), it is required
to have access to the token a particular statement would be directed to.
However, that is pretty difficult to do without access to
calculate_token.

That was initially put in scylladb#508, but planned to put in a separate pr to
keep it minimal
(scylladb#508 (comment))

It seems I had forgotten to open that separate PR, and I end up getting
bitten by that now that I want to constitute my batches in a smarter
way.

So here it is.

I'm only putting "helps" above because I think we may want to also
expose query planning (
`session.plan(prepared_statement, serialized_values) -> impl Iterator<Item = (Arc<Node>, ShardID)>`
) as that may make it significantly easier - but I'd like to keep this
PR that just *enables* the ideal behavior as simple as possible.
@Ten0 Ten0 mentioned this pull request Mar 11, 2023
6 tasks
Ten0 added a commit to Ten0/scylla-rust-driver that referenced this pull request Jun 3, 2023
Resolves scylladb#468

This is a follow-up on scylladb#508 and scylladb#658:
- To minimize CPU usage related to network operations when inserting a very large number of lines, it is relevant to batch.
- To batch in the most efficient manner, these batches have to be shard-aware. Since scylladb#508, `batch` will pick the shard of the first statement to send the query to. However it is left to the user to constitute the batches in such a way that the target shard is the same for all the elements of the batch.
- This was made *possible* by scylladb#658, but it was still very boilerplate-ish. I was waiting for scylladb#612 to be merged (amazing work btw! 😃) to implement a more direct and factored API (as that would use it).
- This new ~`Session::first_shard_for_statement(self, &PreparedStatement, &SerializedValues) -> Option<(Node, Option<Shard>)>` makes shard-aware batching easy on the users, by providing access to the first node and shard of the query plan.
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.

Token aware load balancing for batches
3 participants