Skip to content

Conversation

emily-emily
Copy link
Contributor

@emily-emily emily-emily commented Jul 2, 2024

Problem

Data plane was not set up.

Solution

Sets up the build process to generate gRPC code for data plane. We also have a basic implementation of upsert that calls the generated function.

This PR also restructures some code to include the implementation of PineconeClient (data.rs and control.rs) under pinecone.

The protobuf file for dataplane is currently committed to the repository due to difficulty with ci; it was originally referenced in a submodule but there is some tricky stuff with auth and submodules in GitHub Actions.

Some dependencies are included in pinecone_sdk/imports/google/api for protoc.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Infrastructure change (CI configs, etc)
  • Non-code change (docs, etc)
  • None of the above: (explain here)

Test Plan

Existing test cases pass.

@emily-emily emily-emily marked this pull request as ready for review July 2, 2024 21:11
Copy link
Contributor

@austin-denoble austin-denoble left a comment

Choose a reason for hiding this comment

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

Really solid work here! I left some comments for consideration, specifically around dealing with the Index host and how we're generating code.

I think we can consider a lot of this as follow up PRs / work and keep this PR focused on getting the data plane established like you've done so far. Another pass by @haruska and @ssmith-pc would be appreciated if possible. 🙏

Comment on lines 128 to 134
let index_host = self.describe_index(name).await?.host;
// prepend with "https://" if not already present
let index_host = if index_host.starts_with("https://") {
index_host
} else {
format!("https://{}", index_host)
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe leaving off the schema and applying the :443 port is how we can specify https, right? At least, we're doing that in a couple other clients rather than using a schema for gRPC. We even strip out the https if it's present in a few cases.

Would it make sense to just do the same thing here?

Also @haruska made changes to the Python SDK recently to support allowing users to provide their own port if necessary: pinecone-io/pinecone-python-client#362

@emily-emily had mentioned it sounds like tonic complains if there's not a schema applied, which seems odd. 🤔

@@ -0,0 +1,404 @@
syntax = "proto3";
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be a question for @haruska. How would we approach moving the proto file itself out of the codebase in the future?

From what I understand, the Rust code is generated from protos as a part of cargo build, so we're not checking the generated code into source like we do with the other clients. Would there be some way to do that with Rust rather than having it done as part of cargo build? Is it preferable to leave it as-is?

Copy link

Choose a reason for hiding this comment

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

I'm hoping we publish proto files and this becomes a non-issue.

We could also have a git submodule to the APIs repo. This is not ideal as the project will not build except for our own GH actions and for folks with access to that private repo.

Another option is to move the tonic_build process to its own binary and only run it on an updated proto file. There is an option to set the codegen output folder. We could simply check in the generated code similar to other SDKs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We currently do have a submodule to the APIs repo that we use for the OpenAPI code, but since we build protos through cargo build we need access in the CI. We weren't sure how to authenticate for it other than including somebody's personal access token.

Checking in the generated code could work, should we experiment with that in a different PR or try to fix it here?

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 experimenting with checking in the generated output in a future PR would be fine, you could create a ticket to keep tabs on that as well. 👍

@ericapywang ericapywang requested review from austin-denoble, ericapywang and ssmith-pc and removed request for ericapywang July 12, 2024 14:44
Copy link
Contributor

@austin-denoble austin-denoble left a comment

Choose a reason for hiding this comment

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

Very nice work on this, I really like this approach for allowing configuration of the index connection. Overall I think we're ready to move forward from here, great work again getting this all established.

namespace: namespace.unwrap_or_default(),
};

let response = match self.connection.upsert(request).await {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to use .map_err here?

Copy link
Contributor

Choose a reason for hiding this comment

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

agree, also not sure we need the clone?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

get_ref() returns a reference to the inner object; if I understand correctly, this won't work as the original variable with ownership of the whole thing goes out of scope at the end of this function and our returned reference won't be valid anymore. Are there performance concerns with the clone?

Comment on lines 120 to 125
pub async fn index(
&self,
host: &str,
secure: Option<bool>,
port: Option<u32>,
) -> Result<Index, PineconeError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function may be a good candidate for some unit test coverage to validate all the different permutations of manipulating the host if we don't have any.

Could be done as a separate piece of work in a follow up.

Comment on lines 145 to 149
let endpoint = if endpoint.contains(":") {
endpoint.to_string()
} else {
format!("{}:{}", endpoint, port)
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on previous, won't endpoint always contain a :?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe endpoint isn't the right name, is there a different name that would communicate it better? This is supposed to store a working copy of something between the host and the endpoint where we add the scheme or port if they aren't provided. The final endpoint should always contain a :.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just mean that the end result of running this:

        let endpoint = if endpoint.contains("://") {
            endpoint.to_string()
        } else {
            let scheme = if secure { "https" } else { "http" };
            format!("{}://{}", scheme, endpoint)
        };

will result in endpoint definitely having a : in it, because it's ensuring that there is a scheme with ://

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh I see, thanks for clarifying. I'm thinking of checking for two colons, since after adding the scheme we know there will be at least 1 colon; if the port was specified, there would now be 2 of them. Does that make sense?

Copy link
Contributor

Choose a reason for hiding this comment

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

The logic makes sense, but it doesn't really "feel" like the right implementation to me. Using ends_with would be an improvement: https://doc.rust-lang.org/std/primitive.str.html#method.ends_with

Copy link
Contributor

Choose a reason for hiding this comment

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

and starts_with would probably be better in the other case

Copy link
Contributor Author

@emily-emily emily-emily Jul 15, 2024

Choose a reason for hiding this comment

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

I added some code to test these with regex ("^[a-zA-Z]+://" and ":\d+$")

Comment on lines 95 to 125
impl PineconeClient {
/// Target an index for data operations.
///
/// ### Arguments
/// * `host: &str` - The host of the index to target.
/// * `secure: Option<bool>` - Whether to use a secure connection. If not provided, defaults to `true`.
/// * `port: Option<u32>` - The port of the index to target. If not provided, defaults to `443`.
///
/// ### Return
/// * `Result<Index, PineconeError>` - A Pinecone index object.
///
/// ### Example
///
/// ```no_run
/// use pinecone_sdk::pinecone::PineconeClient;
/// # use pinecone_sdk::utils::errors::PineconeError;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), PineconeError>{
/// let pinecone = PineconeClient::new(None, None, None, None).unwrap();
///
/// let index = pinecone.index("index-name", None, None).await.unwrap();
/// # Ok(())
/// # }
/// ```
pub async fn index(
&self,
host: &str,
secure: Option<bool>,
port: Option<u32>,
) -> Result<Index, PineconeError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you remind me, what is the situation when endpoint either will or won't already be constructed with a scheme? And what is the use case for not using a secure connection?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Calling describe_index returns the host without the scheme, so I think it would make sense for us to accommodate that along with the case that the scheme is included anyway.

As for the secure connection, I believe this is mainly for Pinecone local. We haven't been able to connect to prod with http but it sounds like there is a use case for it in a local dev setting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. Since this is the primary interface users will use to connect to Pinecone, it might be confusing to present this as a choice they have to make when 99.9% of the time they should just connect securely.

How do we handle this in other SDKs? I vaguely remember we all discussed this last week with @austin-denoble and @haruska so apologies if this is a rehash of a decision that was made.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had a discussion about changing the signature to take the host, and along with the host we included secure and port. Would it make sense to remove secure and port since they can be included in host? @austin-denoble @haruska

Copy link
Contributor

Choose a reason for hiding this comment

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

Like Emily mentioned, the secure flag is primarily to allow development against something like Pinecone local. I think you're right though that this is probably not an option we want to provide more generally at the moment.

In Python there's some similar logic, including a secure flag which is used to configure the gRPC client if you pass a config: https://github.com/pinecone-io/pinecone-python-client/blob/8b7e7de50e72248de683a24ada024cf18e6cdce3/pinecone/grpc/base.py#L101

However, this is kind of buried, and I don't think we really expect users to pass a custom gRPC config. I know Jen has also mentioned wanting to remove it from the interface entirely.

@haruska also added logic for allowing a custom port: https://github.com/pinecone-io/pinecone-python-client/blob/main/pinecone/grpc/base.py#L86-L90

I think we may want to go with something similar to what Jason implemented here if possible.

Comment on lines 161 to 169
let endpoint = Channel::from_shared(host.to_string())
.map_err(|e| PineconeError::ConnectionError { inner: Box::new(e) })?
.tls_config(tls_config)
.map_err(|e| PineconeError::ConnectionError { inner: Box::new(e) })?;

let channel = endpoint
.connect()
.await
.map_err(|e| PineconeError::ConnectionError { inner: Box::new(e) })?;
Copy link
Contributor

Choose a reason for hiding this comment

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

very nice

Comment on lines 89 to 90
.get_ref()
.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need to clone?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without the clone this would return &UpsertError, and I'm not sure that the reference would still be valid after the response object goes out of scope. Is there another way around this?

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 we can use into_inner to avoid the clone and access the inner response object:

        let response = self
            .connection
            .upsert(request)
            .await
            .map_err(|e| PineconeError::UpsertError { inner: Box::new(e) })?
            .into_inner();

Copy link
Contributor

@ssmith-pc ssmith-pc left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for addressing all the feedback

Copy link
Contributor

@austin-denoble austin-denoble left a comment

Choose a reason for hiding this comment

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

Looks great, fantastic work! 🚢

@emily-emily emily-emily merged commit df6f233 into main Jul 15, 2024
@emily-emily emily-emily deleted the emily/generate-dataplane-from-proto branch July 15, 2024 20:00
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.

5 participants