Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
415 lines
11 KiB
Go
415 lines
11 KiB
Go
/*
|
|
*
|
|
* Copyright 2026 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package readyreader_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/internal/grpctest"
|
|
"google.golang.org/grpc/internal/testutils"
|
|
"google.golang.org/grpc/internal/transport/readyreader"
|
|
"google.golang.org/grpc/mem"
|
|
)
|
|
|
|
var (
|
|
defaultTestTimeout = 10 * time.Second
|
|
defaultTestShortTimeout = 10 * time.Millisecond
|
|
)
|
|
|
|
type s struct {
|
|
grpctest.Tester
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
grpctest.RunSubTests(t, s{})
|
|
}
|
|
|
|
// TestReadyReader_NonRawConn verifies that ReadOnReady correctly reads data
|
|
// from a net.Conn that doesn't support non-memory-pinning reads.
|
|
func (s) TestReadyReader_NonRawConn(t *testing.T) {
|
|
data := []byte("hello world")
|
|
reader, writer := net.Pipe()
|
|
go writer.Write(data)
|
|
|
|
pool := mem.DefaultBufferPool()
|
|
readyReader := readyreader.New(reader)
|
|
|
|
bufHandle, n, err := readyReader.ReadOnReady(1024, pool)
|
|
if err != nil {
|
|
t.Fatalf("ReadOnReady() failed: %v", err)
|
|
}
|
|
defer pool.Put(bufHandle)
|
|
|
|
if n != len(data) {
|
|
t.Errorf("n = %d; want %d", n, len(data))
|
|
}
|
|
if !bytes.Equal((*bufHandle)[:n], data) {
|
|
t.Errorf("Read data = %s; want %s", string((*bufHandle)[:n]), string(data))
|
|
}
|
|
}
|
|
|
|
func (s) TestReadyReader_EOF(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "localhost:0")
|
|
if err != nil {
|
|
t.Fatalf("net.Listen failed: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
data := []byte("hello")
|
|
go func() {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err := conn.Write(data); err != nil {
|
|
t.Errorf("Failed to write data: %v", err)
|
|
return
|
|
}
|
|
conn.Close()
|
|
}()
|
|
|
|
conn, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("net.Dial failed: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
pool := mem.DefaultBufferPool()
|
|
rr := readyreader.New(conn)
|
|
res, _, err := rr.ReadOnReady(len(data), pool)
|
|
if err != nil {
|
|
t.Errorf("Failed to read: %v", err)
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(*res, data) {
|
|
t.Errorf("Read data = %s; want %s", string(*res), string(data))
|
|
}
|
|
pool.Put(res)
|
|
|
|
// Since the server closes the TCP connection, the next read should return
|
|
// an io.EOF.
|
|
res, _, err = rr.ReadOnReady(len(data), pool)
|
|
if err != io.EOF {
|
|
t.Errorf("Read after server connection close returned err = %v; want io.EOF", err)
|
|
}
|
|
if res != nil {
|
|
t.Error("ReadOnReady() returned non-nil buffer.")
|
|
}
|
|
}
|
|
|
|
// Tests the behavior of readers wrapping TCP connections. It ensures that read
|
|
// operations do not hold onto an allocated buffer while the connection is idle.
|
|
// Buffers should only be permanently allocated from the pool when data is
|
|
// actually ready on the socket, optimizing memory usage for idle connections.
|
|
func (s) TestReadyReader_TCP(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("This test is only applicable for Linux, as RawConn functionality is not implemented for non-linux platforms.")
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
read func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error)
|
|
}{
|
|
{
|
|
name: "ReadyReader",
|
|
read: func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error) {
|
|
rr := readyreader.New(conn)
|
|
bufHandle, n, err := rr.ReadOnReady(readBufSize, pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer pool.Put(bufHandle)
|
|
res := make([]byte, n)
|
|
copy(res, (*bufHandle)[:n])
|
|
return res, nil
|
|
},
|
|
},
|
|
{
|
|
name: "BufReadyReader",
|
|
read: func(conn net.Conn, pool *trackingBufferPool, readBufSize int) ([]byte, error) {
|
|
rr := readyreader.New(conn)
|
|
bufRR := readyreader.NewBuffered(rr, readBufSize, pool)
|
|
buf := make([]byte, 100)
|
|
n, err := bufRR.Read(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buf[:n], nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout)
|
|
defer cancel()
|
|
ln, err := net.Listen("tcp", "localhost:0")
|
|
if err != nil {
|
|
t.Fatalf("net.Listen failed: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
data := []byte("hello tcp delayed")
|
|
connCh := make(chan net.Conn)
|
|
go func() {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
connCh <- conn
|
|
}()
|
|
|
|
conn, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("net.Dial failed: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
serverConn := <-connCh
|
|
defer serverConn.Close()
|
|
|
|
pool := newTrackingPool(mem.DefaultBufferPool())
|
|
const readBufSize = 1024
|
|
|
|
resCh := make(chan []byte)
|
|
|
|
go func() {
|
|
res, err := tc.read(conn, pool, readBufSize)
|
|
if err != nil {
|
|
t.Errorf("Failed to read: %v", err)
|
|
return
|
|
}
|
|
resCh <- res
|
|
}()
|
|
|
|
// Verify that no read buffer is allocated for a short while. If it
|
|
// is allocated (e.g. for a probe), it must be returned immediately.
|
|
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
|
defer sCancel()
|
|
if n, err := pool.requestChan.Receive(sCtx); err == nil {
|
|
if n != readBufSize {
|
|
t.Fatalf("Unexpected request buffer size: got %d, want %d", n, readBufSize)
|
|
}
|
|
if n, err := pool.putChan.Receive(ctx); err != nil {
|
|
t.Fatal("Read buffer allocated and NOT returned while idle.")
|
|
} else if n != readBufSize {
|
|
t.Fatalf("Unexpected returned buffer size: got %d, want %d", n, readBufSize)
|
|
}
|
|
}
|
|
|
|
// Write data to unblock.
|
|
serverConn.Write(data)
|
|
|
|
if n, err := pool.requestChan.Receive(ctx); err != nil {
|
|
t.Fatal("Context timed out while waiting for a read buffer to be allocated.")
|
|
} else if n != readBufSize {
|
|
t.Fatalf("Unexpected request buffer size: got %d, want %d", n, readBufSize)
|
|
}
|
|
|
|
var res []byte
|
|
select {
|
|
case res = <-resCh:
|
|
case <-ctx.Done():
|
|
t.Fatal("Context timed out waiting for read to complete.")
|
|
}
|
|
if !bytes.Equal(res, data) {
|
|
t.Errorf("Read data = %s; want %s", string(res), string(data))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// trackingBufferPool wraps a mem.BufferPool and provides channels to track
|
|
// when buffers are requested and returned, useful for verifying allocation
|
|
// behavior in tests.
|
|
type trackingBufferPool struct {
|
|
mem.BufferPool
|
|
requestChan testutils.Channel
|
|
putChan testutils.Channel
|
|
}
|
|
|
|
func newTrackingPool(pool mem.BufferPool) *trackingBufferPool {
|
|
return &trackingBufferPool{
|
|
BufferPool: pool,
|
|
requestChan: *testutils.NewChannelWithSize(1),
|
|
putChan: *testutils.NewChannelWithSize(1),
|
|
}
|
|
}
|
|
|
|
func (p *trackingBufferPool) Get(size int) *[]byte {
|
|
p.requestChan.Replace(size)
|
|
return p.BufferPool.Get(size)
|
|
}
|
|
|
|
func (p *trackingBufferPool) Put(b *[]byte) {
|
|
p.putChan.Replace(len(*b))
|
|
p.BufferPool.Put(b)
|
|
}
|
|
|
|
// Call Read to accumulate the text of a file
|
|
func reads(buf io.Reader, m int) string {
|
|
var b [1000]byte
|
|
nb := 0
|
|
for {
|
|
n, err := buf.Read(b[nb : nb+m])
|
|
nb += n
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
return string(b[0:nb])
|
|
}
|
|
|
|
type bufReader struct {
|
|
name string
|
|
fn func(io.Reader) string
|
|
}
|
|
|
|
var bufreaders = []bufReader{
|
|
{"1", func(b io.Reader) string { return reads(b, 1) }},
|
|
{"2", func(b io.Reader) string { return reads(b, 2) }},
|
|
{"3", func(b io.Reader) string { return reads(b, 3) }},
|
|
{"4", func(b io.Reader) string { return reads(b, 4) }},
|
|
{"5", func(b io.Reader) string { return reads(b, 5) }},
|
|
{"7", func(b io.Reader) string { return reads(b, 7) }},
|
|
}
|
|
|
|
const minReadBufferSize = 16
|
|
|
|
var bufsizes = []int{
|
|
0, minReadBufferSize, 23, 32, 46, 64, 93, 128, 1024, 4096,
|
|
}
|
|
|
|
func (s) TestBufReader(t *testing.T) {
|
|
var texts [31]string
|
|
str := ""
|
|
all := ""
|
|
for i := range len(texts) - 1 {
|
|
texts[i] = str + "\n"
|
|
all += texts[i]
|
|
str += string(rune(i%26 + 'a'))
|
|
}
|
|
texts[len(texts)-1] = all
|
|
|
|
for _, text := range texts {
|
|
for _, bufreader := range bufreaders {
|
|
for _, bufsize := range bufsizes {
|
|
// We don't use t.Run() here to avoid excessive logging due to
|
|
// the large number of subtests.
|
|
read := readyreader.New(strings.NewReader(text))
|
|
buf := readyreader.NewBuffered(read, bufsize, mem.DefaultBufferPool())
|
|
s := bufreader.fn(buf)
|
|
if s != text {
|
|
t.Errorf("fn=%s bufsize=%d want=%q got=%q", bufreader.name, bufsize, text, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s) TestBufReader_ReadEmptyBuffer(t *testing.T) {
|
|
rr := readyreader.New(new(bytes.Buffer))
|
|
l := readyreader.NewBuffered(rr, minReadBufferSize, mem.DefaultBufferPool())
|
|
b := make([]byte, 100)
|
|
n, err := l.Read(b)
|
|
if err != io.EOF {
|
|
t.Errorf("expected EOF from Read, got %q %v", b[:n], err)
|
|
}
|
|
}
|
|
|
|
type errorThenGoodReader struct {
|
|
didErr bool
|
|
nread int
|
|
}
|
|
|
|
var errFake = errors.New("fake error")
|
|
|
|
func (r *errorThenGoodReader) Read(p []byte) (int, error) {
|
|
r.nread++
|
|
if !r.didErr {
|
|
r.didErr = true
|
|
return 0, errFake
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (s) TestBufReader_ClearError(t *testing.T) {
|
|
r := &errorThenGoodReader{}
|
|
b := readyreader.NewBuffered(readyreader.New(r), minReadBufferSize, mem.DefaultBufferPool())
|
|
buf := make([]byte, 1)
|
|
if _, err := b.Read(nil); err != nil {
|
|
t.Fatalf("1st nil Read = %v; want nil", err)
|
|
}
|
|
if _, err := b.Read(buf); err != errFake {
|
|
t.Fatalf("1st Read = %v; want errFake", err)
|
|
}
|
|
if _, err := b.Read(nil); err != nil {
|
|
t.Fatalf("2nd nil Read = %v; want nil", err)
|
|
}
|
|
if _, err := b.Read(buf); err != nil {
|
|
t.Fatalf("3rd Read with buffer = %v; want nil", err)
|
|
}
|
|
if r.nread != 2 {
|
|
t.Errorf("num reads = %d; want 2", r.nread)
|
|
}
|
|
}
|
|
|
|
type emptyThenNonEmptyReader struct {
|
|
r io.Reader
|
|
n int
|
|
}
|
|
|
|
func (r *emptyThenNonEmptyReader) Read(p []byte) (int, error) {
|
|
if r.n <= 0 {
|
|
return r.r.Read(p)
|
|
}
|
|
r.n--
|
|
return 0, nil
|
|
}
|
|
|
|
func (s) TestBufReader_ReadZero(t *testing.T) {
|
|
for _, size := range []int{100, 2} {
|
|
t.Run(fmt.Sprintf("bufsize=%d", size), func(t *testing.T) {
|
|
r := io.MultiReader(strings.NewReader("abc"), &emptyThenNonEmptyReader{r: strings.NewReader("def"), n: 1})
|
|
br := readyreader.NewBuffered(readyreader.New(r), size, mem.DefaultBufferPool())
|
|
want := func(s string, wantErr error) {
|
|
p := make([]byte, 50)
|
|
n, err := br.Read(p)
|
|
if err != wantErr || n != len(s) || string(p[:n]) != s {
|
|
t.Fatalf("read(%d) = %q, %v, want %q, %v", len(p), string(p[:n]), err, s, wantErr)
|
|
}
|
|
t.Logf("read(%d) = %q, %v", len(p), string(p[:n]), err)
|
|
}
|
|
want("abc", nil)
|
|
want("", nil)
|
|
want("def", nil)
|
|
want("", io.EOF)
|
|
})
|
|
}
|
|
}
|