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

New Backend: Apache Ignite #146

Merged
merged 16 commits into from
Dec 6, 2023
Merged

New Backend: Apache Ignite #146

merged 16 commits into from
Dec 6, 2023

Conversation

guscarreon
Copy link
Contributor

No description provided.

@hhhjort hhhjort self-requested a review August 23, 2023 17:10
@hhhjort hhhjort self-assigned this Aug 23, 2023
@hhhjort hhhjort requested a review from AlexBVolcy August 23, 2023 17:12
@@ -155,6 +155,13 @@ func TestNewBaseBackend(t *testing.T) {
{msg: "Error creating Redis backend: ", lvl: logrus.FatalLevel},
},
},
//{

Choose a reason for hiding this comment

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

Can this commented out code be deleted?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It probably needs to be uncommented, and perhaps worked on if it isn't in working condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Corrected

return "", err
}

// Unmarshall response

Choose a reason for hiding this comment

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

Very Minor Nitpick: Spelling Unmarshall --> Unmarshal

return "", utils.NewPBCError(utils.GET_INTERNAL_SERVER, igniteResponse.Error)
} else if igniteResponse.Status > 0 {
return "", utils.NewPBCError(utils.GET_INTERNAL_SERVER, "Ignite response.Status not zero")
} else if len(igniteResponse.Response) == 0 { // both igniteResponse.Status == 0 && len(igniteResponse.Error) == 0

Choose a reason for hiding this comment

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

I think you can delete the comment here

return false, err
}

// Unmarshall response

Choose a reason for hiding this comment

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

Same spelling nitpick

}

// Validate response
if igniteResponse.Status > 0 || len(igniteResponse.Error) > 0 {

Choose a reason for hiding this comment

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

Can this be combined into igniteResponse.Response like you did in getResponse validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This validation changed


// NewIgniteBackend expects a valid config.IgniteBackend object
func NewIgniteBackend(cfg config.Ignite) *IgniteBackend {

Choose a reason for hiding this comment

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

Nitpick: Can you remove this blank line

// Get creates an Get http.Request with the URL query values needed to perform a "get" operation
// on an Ignite storage instance using the REST API
func (back *IgniteBackend) Get(ctx context.Context, key string) (string, error) {

Choose a reason for hiding this comment

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

Nitpick: Can you remove this blank line


// Get creates an Get http.Request with the URL query values needed to perform a "get" operation
// on an Ignite storage instance using the REST API
func (back *IgniteBackend) Get(ctx context.Context, key string) (string, error) {

Choose a reason for hiding this comment

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

I think back can be renamed to ig, what do you think?

utils/errors.go Outdated
@@ -18,6 +18,7 @@ const (
KEY_NOT_FOUND // GET http.StatusNotFound 404
KEY_LENGTH // GET http.StatusNotFound 404
UNKNOWN_STORED_DATA_TYPE // GET http.StatusInternalServerError 500
GET_INTERNAL_SERVER // PUT http.StatusInternalServerError 500

Choose a reason for hiding this comment

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

Should the comment here say // GET http.StatusInternalServerError 500?


// Get implements the IgniteDB interface Get method and makes the Ignite storage client retrieve
// the value that has been previously stored under 'key' if its TTL is still current. We can tell
// when a key is not faound when Ignite doesn't return an error, nor a 'Status' different than zero, but
Copy link
Collaborator

Choose a reason for hiding this comment

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

faound -> found

@bsardo bsardo assigned bsardo and unassigned hhhjort Sep 28, 2023
@bsardo bsardo assigned hhhjort and unassigned bsardo Oct 5, 2023

// putResponse is used to unmarshal the Ignite server's response to a PUT request with
// the "cmd" URL query field set to "putifabs"
type putResponse struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason for defining both a getResponse and a putResponse when they both seem to be the same structure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The difference is that in putResponse.Response is a bool and getResponse.Response is a string. Do you want me to try to consolidate them into a single struct?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good observation by @hhhjort but I think keeping them as separate structs due to the type difference on Response makes sense.

Scheme string `mapstructure:"scheme"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Secure bool `mapstructure:"secure"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this should be renamed VerifyCert or some documentation added to explain that secure in this context means verifying the SSL certificate on the ignite server. On the face of it Secure sounds like it may be redundant with Scheme, or suggest that there is a third possibility for Scheme that may or may not use 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.

Modified and added a comment

@bsardo bsardo assigned bsardo and unassigned AlexBVolcy Oct 23, 2023
hhhjort
hhhjort previously approved these changes Oct 23, 2023
host: "127.0.0.1"
port: 8080
secure: false
headers: !!omap
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is !!omap needed here? It looks like this is just to ensure order is preserved when loading the file but there is only one element.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe we need it to read the headers as map[String]string

log.Infof("config.backend.ignite.host: %s", cfg.Host)
log.Infof("config.backend.ignite.port: %d", cfg.Port)
log.Infof("config.backend.ignite.cache.name: %s", cfg.Cache.Name)
log.Infof("config.backend.ignite.cache.create_on_start: %v", cfg.Cache.CreateOnStart)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this supposed to be %t instead of %v?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Corrected

Comment on lines +184 to +185
VerifyCert bool `mapstructure:"secure"`
Headers map[string]string `mapstructure:"headers"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

These don't have log.Infof statements in validateAndLog. I'm not sure if that's intentional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

}

if httpResp.StatusCode != http.StatusOK {
httpErr = fmt.Errorf("Ignite error. Unexpected status code: %d", httpResp.StatusCode)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we able to just return here when this error is detected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we return here, we'd get an entry in the logs with the status code alone, which is not that helpful. The logic behind not exiting here is to append more information to httpErr such as the error message found in httpResp.Body or lack thereof.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I see. I can get on board with this approach, however, I'm curious if/when the additional details are needed. For example, typically if the status code is 204 that means that the request was fulfilled without a body so if that is a status code that Ignite might send back, I don't see why we might need to append "Received empty httpResp.Body" to the error message. Shouldn't we be able to just report back that status code 204 was received from Ignite and rely on that status code's meaning to tell the story?

I guess I'm asking if you've observed a need to provide this additional information when the status code is not 200 when working with a live Ignite server. In most cases the semantics of a HTTP code provide the information we need. Furthermore, I'm curious if you've observed any instances where Ignite provides an error message in the body along with the non-200 status code. If so, would that suffice?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Discussed offline. The Ignite REST API docs imply that all responses are 200s and we have yet to encounter a non-200 response when working with a live Ignite server. Any error details are expected to be in the response body. As such, I'm good with the current design which is focused on receiving and unpacking a response while catching transport and response body errors but also accounting for the possibility of non-200 error codes. A non-200 error code is returned but only after we've attempted to read the response body since there might be more information there.

Comment on lines +62 to +68
if httpResp.Body == nil {
errMsg := "Received empty httpResp.Body"
if httpErr == nil {
return nil, fmt.Errorf("Ignite error. %s", errMsg)
}
return nil, fmt.Errorf("%s; %s", httpErr.Error(), errMsg)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we decide it makes sense to return on line 59 above then I think this logic can be simplified to:

if httpResp.Body == nil {
    return nil, errors.New("Ignite error. Received empty body")
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking about keeping the logic above in order to potentially have more helpful error logs. Do you agree?

igniteResponse := getResponse{}

if unmarshalErr := json.Unmarshal(responseBytes, &igniteResponse); unmarshalErr != nil {
return fmt.Errorf("Unmarshal response error: %s; Response body: %s", unmarshalErr.Error(), string(responseBytes))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just curious, is this error message style consistent with the rest of PBC? This error would ultimately be logged as "Error creating Ignite backend: Unmarshal response error: [some error] Response body: [body bytes]" by the calling code.

Copy link
Contributor Author

@guscarreon guscarreon Nov 6, 2023

Choose a reason for hiding this comment

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

It's kind of long but, this is the only Unmarshal error that PBC handles. We are currently using Ignite error for server side errors, validation errors look like: Error creating Ignite backend, so Unmarshal response error seemed reasonable for me. Should we keep messages shorter?

igniteResponse := getResponse{}

if unmarshalErr := json.Unmarshal(responseBytes, &igniteResponse); unmarshalErr != nil {
return "", fmt.Errorf("Unmarshal response error: %s; Response body: %s", unmarshalErr.Error(), string(responseBytes))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should an unmarshal error be considered to be of type utils.GET_INTERNAL_SERVER?

Copy link
Contributor Author

@guscarreon guscarreon Nov 6, 2023

Choose a reason for hiding this comment

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

Good question. GET_INTERNAL_SERVER and PUT_INTERNAL_SERVER are being used to label errors that happen on the backend side. Given that this particular error happens on PBC, I decided to keep things simple and not label this error as INTERNAL_SERVER but, I realize that a malformed JSON blob could be consequence of a server side error. Maybe a JSON validation error should be considered a GET_INTERNAL_SERVER error. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm I was thinking an unmarshal failure here can be attributed to a malformed response from the Ignite server, in which case I would consider this a GET_INTERNAL_SERVER error. I'm curious what @hhhjort thinks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you are right. Modified


// putResponse is used to unmarshal the Ignite server's response to a PUT request with
// the "cmd" URL query field set to "putifabs"
type putResponse struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good observation by @hhhjort but I think keeping them as separate structs due to the type difference on Response makes sense.

return utils.NewPBCError(utils.PUT_INTERNAL_SERVER, "Ignite responded with non-zero successStatus code")
}

if !igniteResponse.Response {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the ! supposed to be here? If igniteResponse.Response is true a record already exists right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that we are not rewritting values for existing keys, we are using Apache Ignite's putifabsent command. According to the put-if-absent docs: "The response field contains true if the entry was put, false otherwise."

desc: "config validation error",
testCases: []testCase{
{
desc: "empty scheme",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick: for the "empty scheme", "empty host" and "empty port" tests, I think it would be best if all of the values were valid except for a single field so we can be sure the field under test is what is causing the error. The error message is the same for all of these empty fields so it is not possible to discern what is causing the failure as the test is currently constructed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@bsardo bsardo merged commit 58cffa3 into master Dec 6, 2023
3 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.

4 participants