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 receiving empty messages when compression is enabled and maxReceiveMessageSize is maxInt64 #7753

Conversation

misvivek
Copy link

@misvivek misvivek commented Oct 17, 2024

Fixes #4552

RELEASE NOTES:

  • grpc: fix receiving empty messages when compression is enabled and maxReceiveMessageSize is maxInt64

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.98%. Comparing base (c63aeef) to head (f3afb78).
Report is 9 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7753      +/-   ##
==========================================
+ Coverage   81.80%   81.98%   +0.17%     
==========================================
  Files         375      377       +2     
  Lines       37978    38132     +154     
==========================================
+ Hits        31068    31262     +194     
+ Misses       5609     5564      -45     
- Partials     1301     1306       +5     
Files with missing lines Coverage Δ
rpc_util.go 79.94% <100.00%> (+0.62%) ⬆️

... and 33 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?

@misvivek misvivek requested a review from purnesh42H November 7, 2024 04:05
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
@misvivek misvivek requested a review from purnesh42H November 7, 2024 20:29
rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
{
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

rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
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.

rpc_util_test.go Outdated
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

rpc_util_test.go Outdated
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

@misvivek misvivek requested a review from purnesh42H November 19, 2024 02:55
@purnesh42H purnesh42H changed the title grpc: fix getting empty response when compress enabled and maxReceiveMessageSize be maxInt64 grpc: fix receiving empty messages when compression is enabled and maxReceiveMessageSize is maxInt64 Nov 20, 2024
rpc_util.go Outdated
@@ -900,12 +899,33 @@ func decompress(compressor encoding.Compressor, d mem.BufferSlice, maxReceiveMes
//}

var out mem.BufferSlice
_, err = io.Copy(mem.NewWriter(&out, pool), io.LimitReader(dcReader, int64(maxReceiveMessageSize)+1))
_, err = io.Copy(mem.NewWriter(&out, pool), io.LimitReader(dcReader, int64(maxReceiveMessageSize)))
Copy link
Contributor

Choose a reason for hiding this comment

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

@misvivek can you rebase with latest main branch. This has changed to mem.ReadAll. Can you verify if bug still exist?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please verify if the bug still exist first with me.ReadAll. Don't change that

rpc_util_test.go Outdated
error: errors.New("overflow: received message size is larger than the allowed maxReceiveMessageSize (5 bytes)"),
},
{
name: "Succeeds when message size is much smaller than maxReceiveMessageSize",
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 similar to case 1. We don't 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

rpc_util_test.go Outdated

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
message := 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.

this is a strange way of initializing. It should be done something like below

mem.BufferSlice{mem.NewBuffer(&input, nil)}

Copy link
Author

@misvivek misvivek Nov 20, 2024

Choose a reason for hiding this comment

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

we have to send the compressed message, so we are doing the above way,
we tried your suggestion then we are getting like gzip: invalid header,

rpc_util_test.go Outdated
return mem.BufferSlice{mem.NewBuffer(&compressedData, nil)}
}()
output, size, err := decompress(tt.compressor, message, tt.maxReceiveMessageSize, nil)
wantMsg := 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. Initialize as suggested above.

decompressed := tt.want
return mem.BufferSlice{mem.NewBuffer(&decompressed, nil)}
}()

Copy link
Contributor

Choose a reason for hiding this comment

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

no need of new lines when checking different things. Remove them please.

},
}

for _, tt := range tests {
Copy link
Contributor

Choose a reason for hiding this comment

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

s/tt/test

rpc_util_test.go Outdated
}()

// Check for expected error
if (err != nil) != (tt.error != nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

it should be more clear. Something like if test.error != nil && err == nil then throw error that we got nil error when we want non-nil err

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

}
// to check when we are passing empty bytes output is "nil" failure, empty receive message case
if wantMsg != nil && output != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

there should be separate condition to check if wanMsg is non-nil and output is nil. Then it should throw error that got nil when we want non-nil

@misvivek misvivek force-pushed the 4552-empty-response-on-enable-decompression branch from 47003c3 to fad8892 Compare November 21, 2024 05:36
rpc_util_test.go Outdated
if wantMsg != nil && output == nil {
t.Fatalf("decompress() got = nil, want = non nil")
}
if wantMsg != nil && output != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

no, need of checking non nil again for output as you already checked that

@misvivek misvivek requested a review from purnesh42H November 28, 2024 08:30
@purnesh42H purnesh42H modified the milestones: 1.69 Release, 1.70 Release Dec 5, 2024
@dfawley dfawley closed this Dec 10, 2024
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.

Get empty response when compress enabled and maxReceiveMessageSize be maxInt64
3 participants