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

grpc: fix getting empty response when compress enabled and maxReceiveMessageSize be maxInt64 #7753

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

Conversation

misvivek
Copy link

@misvivek misvivek commented Oct 17, 2024

#issue no: #4552
Get empty response when compress enabled and maxReceiveMessageSize be maxInt64
Action:

UT Successful decompression
UT Decompressed size exceeds maxReceiveMessageSize
UT Error during decompression
UT Buffer overflow
make the decompress function compatible with calling function

Release note:

NA

Copy link

linux-foundation-easycla bot commented Oct 17, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@purnesh42H purnesh42H changed the title 4552 empty response on enable decompression grpc: fix getting empty response when compress enabled and maxReceiveMessageSize be maxInt64 Oct 17, 2024
@purnesh42H
Copy link
Contributor

@misvivek please keep the description and pr title in the format as I have updated

@misvivek
Copy link
Author

noted @purnesh42H

rpc_util.go Show resolved Hide resolved
rpc_util.go Outdated
if err = checkReceiveMessageOverflow(int64(out.Len()), int64(maxReceiveMessageSize), dcReader); err != nil {
return nil, out.Len() + 1, err
}
return out, len(out), err
Copy link
Contributor

Choose a reason for hiding this comment

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

return out.Len()

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util.go Outdated
if readBytes == maxReceiveMessageSize {
b := make([]byte, 1)
if n, err := dcReader.Read(b); n > 0 || err != io.EOF {
return fmt.Errorf("overflow: message larger than max size receivable by client (%d bytes)", maxReceiveMessageSize)
Copy link
Contributor

Choose a reason for hiding this comment

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

"overflow: received message size is larger than the allowed maxReceiveMessageSize (%d bytes)."

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
@@ -290,3 +292,130 @@ func BenchmarkGZIPCompressor512KiB(b *testing.B) {
func BenchmarkGZIPCompressor1MiB(b *testing.B) {
bmCompressor(b, 1024*1024, NewGZIPCompressor())
}

type mockCompressor struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

s/mockCompressor/testCompressor

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
@@ -290,3 +292,130 @@ func BenchmarkGZIPCompressor512KiB(b *testing.B) {
func BenchmarkGZIPCompressor1MiB(b *testing.B) {
bmCompressor(b, 1024*1024, NewGZIPCompressor())
}

type mockCompressor struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

top level docstring explaining the use of this testCompressor and top level comments for all the functions

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
t.Errorf("decompress() size = %d, want %d", size, tt.size)
}
if len(tt.want) != len(output) {
t.Errorf("decompress() output length = %d, want %d", output, tt.want)
Copy link
Contributor

Choose a reason for hiding this comment

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

decompress() output length, got = %d, want = %d

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
name: "Successful decompression",
compressor: &mockCompressor{
DecompressedData: []byte("decompressed data"),
Size: 17,
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 you need "Size" as different attribute, can't you calculate from DecompressedData?

rpc_util_test.go Outdated
@@ -290,3 +292,130 @@ func BenchmarkGZIPCompressor512KiB(b *testing.B) {
func BenchmarkGZIPCompressor1MiB(b *testing.B) {
bmCompressor(b, 1024*1024, NewGZIPCompressor())
}

type mockCompressor struct {
DecompressedData []byte
Copy link
Contributor

Choose a reason for hiding this comment

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

why are these attributes upper case? We are not exporting them

Copy link
Author

Choose a reason for hiding this comment

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

changed to lowercase

rpc_util_test.go Outdated
input mem.BufferSlice
maxReceiveMessageSize int
want mem.BufferSlice
size int
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this size for? Please give more meaningful name

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
}{
{
name: "Successful decompression",
compressor: &mockCompressor{
Copy link
Contributor

Choose a reason for hiding this comment

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

do we really need to test compressor for all cases? Can't we just use a regular compressor?

Copy link
Contributor

Choose a reason for hiding this comment

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

Part of this test is to also verify that inputs work with regular compressor

Copy link
Author

Choose a reason for hiding this comment

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

Something like this @purnesh42H

``compressor := gzip.NewCompressor()
originalData := []byte("test data")

// Compress
compressedData, err := compressor.Compress(originalData)
if err != nil {
	t.Fatalf("Failed to compress data: %v", err)
}

// Decompress
decompressedData, err := compressor.Decompress(compressedData)
if err != nil {
	t.Fatalf("Failed to decompress data: %v", err)
}

if !bytes.Equal(decompressedData, originalData) {
	t.Errorf("Decompressed data does not match original")
}``

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah i think we should try to get the regular test cases working with regular compressor. Only use testCompressor when we want to simulate or enforce an error

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 check this and see how are different compressors are tested https://github.com/grpc/grpc-go/blob/56df169480cdb4928a24a50b5289f909f0d81ba7/test/compressor_test.go ?

We should try to do something similar. There are test compressors as well regular compressors based on test cases. Compressor can be part of test case struct

Copy link
Contributor

Choose a reason for hiding this comment

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

One more example

func (s) TestCompress(t *testing.T) {

Copy link
Contributor

@purnesh42H purnesh42H left a comment

Choose a reason for hiding this comment

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

Let's try to make as many as tests as we can using regular compressor and then we can review the rest

rpc_util_test.go Outdated
// used for testing compression and decompression functionality.
// Compress is a placeholder for the compression logic.
// It currently returns a nil WriteCloser and no error, as the actual compression is not implemented.
// DecompressedSize returns the pre-configured size of the decompressed data.
Copy link
Contributor

Choose a reason for hiding this comment

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

these comments should be against the attributes of struct, not in docstring

rpc_util_test.go Outdated
}{
{
name: "Successful decompression",
compressor: &mockCompressor{
Copy link
Contributor

Choose a reason for hiding this comment

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

yeah i think we should try to get the regular test cases working with regular compressor. Only use testCompressor when we want to simulate or enforce an error

Copy link

codecov bot commented Oct 22, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.85%. Comparing base (b3393d9) to head (9625cd6).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7753      +/-   ##
==========================================
- Coverage   81.91%   81.85%   -0.06%     
==========================================
  Files         373      373              
  Lines       37844    37855      +11     
==========================================
- Hits        31000    30987      -13     
- Misses       5551     5576      +25     
+ Partials     1293     1292       -1     
Files with missing lines Coverage Δ
rpc_util.go 80.85% <100.00%> (+1.32%) ⬆️

... and 19 files with indirect coverage changes

rpc_util_test.go Outdated
t.Fatalf("Could not initialize gzip compressor with level 5 compression.")
}

for _, test := range []struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

@misvivek i don't understand what this test loop is doing and what is it testing. This seems totally unnecessary for the change (line 349-373)

rpc_util_test.go Outdated
}{
{
name: "Successful decompression",
compressor: &testCompressor{
Copy link
Contributor

Choose a reason for hiding this comment

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

you have to use real compressor here. I have mentioned this so many times. Why do you keep using test compressor here?

rpc_util_test.go Outdated
@@ -290,3 +293,129 @@ func BenchmarkGZIPCompressor512KiB(b *testing.B) {
func BenchmarkGZIPCompressor1MiB(b *testing.B) {
bmCompressor(b, 1024*1024, NewGZIPCompressor())
}

type testCompressor struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

this testCompressor should not be used ideally. If at all we are using it, we should not use it for test cases involving max receive size and actual receive message size

Copy link
Author

Choose a reason for hiding this comment

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

done

rpc_util_test.go Outdated
error error
}{
{
name: "Successful decompression",
Copy link
Contributor

Choose a reason for hiding this comment

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

add more context to name. For example, in this case actual message size is less than allowed receive size. Keep it short though.

rpc_util_test.go Outdated
error: nil,
},
{
name: "Error during decompression",
Copy link
Contributor

Choose a reason for hiding this comment

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

same here. name should be meaningful in terms of input and output

rpc_util_test.go Outdated
error: errors.New("decompression error"),
},
{
name: "Buffer overflow",
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a better name

rpc_util_test.go Outdated
},
{
name: "Buffer overflow",
compressor: &testCompressor{
Copy link
Contributor

Choose a reason for hiding this comment

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

testCompressor should not be required here

rpc_util_test.go Outdated
},
{
name: "MaxInt64 receive size with small data",
compressor: &testCompressor{
Copy link
Contributor

Choose a reason for hiding this comment

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

again, testCompressor should not be required here. We should use a regular compressor and simulate a scenario where receiveMaxSize is Int64 and actual data is small and that should pass.

@misvivek misvivek force-pushed the 4552-empty-response-on-enable-decompression branch from 3589636 to 3603069 Compare November 7, 2024 19:55
error error
}{
{
name: "Decompresses successfully with sufficient buffer size",
Copy link
Contributor

Choose a reason for hiding this comment

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

success, receive message within maxReceiveMessageSize

Copy link
Author

Choose a reason for hiding this comment

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

done

error: errors.New("decompression error"),
},
{
name: "Fails with overflow error when decompressed size exceeds maxReceiveMessageSize",
Copy link
Contributor

Choose a reason for hiding this comment

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

overflow failure, receive message exceeds maxReceiveMessageSize

Copy link
Author

Choose a reason for hiding this comment

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

done

error: nil,
},
{
name: "Fails with io.Copy error during decompression",
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this test case? Do we need it?

Copy link
Author

Choose a reason for hiding this comment

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

done removed

error: errors.New("overflow: received message size is larger than the allowed maxReceiveMessageSize (5 bytes)."),
},
{
name: "Returns EOF error when maxReceiveMessageSize is MaxInt64 but data ends prematurely",
Copy link
Contributor

Choose a reason for hiding this comment

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

EOF is not really failure. This test case is similar to case 1) except maxReceiveMessageSize is math.MAXInt64

Copy link
Author

Choose a reason for hiding this comment

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

done

compressedSize: 0,
error: errors.New("simulated io.Copy read error"),
},
{
Copy link
Contributor

Choose a reason for hiding this comment

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

this is redundant. Case 1) already test it?

Copy link
Author

Choose a reason for hiding this comment

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

done remove

{
name: "Decompresses successfully with sufficient buffer size",
compressor: c,
input: func() mem.BufferSlice {
Copy link
Contributor

Choose a reason for hiding this comment

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

As suggested, change this to []byte and do mem.BufferSlice when calling decompress

return mem.BufferSlice{mem.NewBuffer(&compressedData, nil)}
}(),
maxReceiveMessageSize: 100,
want: func() mem.BufferSlice {
Copy link
Contributor

Choose a reason for hiding this comment

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

same here. this should be just []byte. When comparing you should compare to this in []byte format

if size != tt.compressedSize {
t.Errorf("decompress() size, got = %d, want = %d", size, tt.compressedSize)
}
if len(tt.want) != len(output) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you should compare the actual bytes instead of just comparing the length.

output, size, err := decompress(tt.compressor, tt.input, tt.maxReceiveMessageSize, nil)
// Check if both err and tt.error are either nil or non-nil
if (err != nil) != (tt.error != nil) {
t.Errorf("decompress() error, got err=%v, want err=%v", err, tt.error)
Copy link
Contributor

Choose a reason for hiding this comment

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

all should change to t.Fatalf because we don't want to continue once detecting an error. Like you won't compare compressed size if decompression has returned error

input mem.BufferSlice
maxReceiveMessageSize int
want mem.BufferSlice
compressedSize int
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 you need compressedSize as parameter here. It can be easily calculated from compressed data you are providing.

Copy link
Author

@misvivek misvivek Nov 8, 2024

Choose a reason for hiding this comment

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

compressedSize := 0 for _, buf := range tt.input { compressedSize += len(buf.Bytes()) } your are suggesting like this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants