alts: Release read buffer when blocked on socket read (#8964)
## Problem
Normally, since the Go `net.Conn` interface provides the abstraction of
a blocking read to hide the complexity of non-blocking I/O (epoll,
kqueue), users need to pass a read buffer to the `net.Conn.Read` call.
Even when the TCP socket doesn't have data, the application needs to
hold onto the read buffer. This results in the ALTS conn and the gRPC
HTTP/2 stack holding on to 32KB read buffers each, even for non-readable
transports.
## Solution
On Unix platforms, there is a
[RawConn](https://pkg.go.dev/syscall#RawConn#Read) interface that
exposes a non-blocking mechanism. The idea is that the Go runtime will
call a user-registered callback when the socket is readable. gRPC can
use this callback to allocate a buffer from the pool and return it once
it has passed the plaintext to the HTTP/2 layer. The same RawConn
interface is also available in other OSs, but with slightly different
method signatures for the blocking `Read` method. In the future, we can
add the same optimization for them and have CI runners to catch
regressions.
The main abstraction that allows these non-memory-pinning reads is the
following interface:
```go
// ReadyReader is an optional interface that can be implemented by net.Conn
// implementations to enable gRPC to perform non-memory-pinning reads.
type ReadyReader interface {
// ReadOnReady waits for data to arrive, fetches a buffer, and performs a
// read. It returns a pointer to the buffer so you can return it to the pool
// later.
ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error)
}
```
In this PR, an implementation is provided that wraps a `RawConn`. This
allows the ALTS conn to perform efficient reads.
In a future PR, the following changes will enable getting rid of the
bufio.Reader in the HTTP/2 layer:
1. `ReadyReader` will be implemented by the ALTS conn.
2. gRPC will implement its own buffered reader that releases the buffer
when it's empty.
3. The buffered reader will call `ReadOnReady()` instead of `Read()` on
the underlying conn, if supported, to delay the re-allocation of the
buffer.
## Benchmarks
The following micro-benchmarks show no regression in QPS (LargeMessage
test) and a significant reduction in memory usage while performing reads
(ReadMemoryUsage). There is an increase in 2 allocs in conn construction
due to the use of pointer fields for the `ReadyReader` and read buffer
handle, but these happen only when creating a subchannel, not per-RPC.
There is an increase in sec/op for the `WriteMemoryUsage` test, but
these tests are not meant to measure conn construction time, only the
memory effeciency.
```
goos: linux
goarch: amd64
pkg: google.golang.org/grpc/credentials/alts/internal/conn
cpu: Intel(R) Xeon(R) CPU @ 2.60GHz
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
LargeMessage-48 77.54m ± 2% 76.51m ± 1% ~ (p=0.202 n=15)
WriteMemoryUsage-48 6.816µ ± 1% 7.519µ ± 1% +10.31% (p=0.000 n=15)
ReadMemoryUsage-48 9.754µ ± 1% 7.021µ ± 1% -28.02% (p=0.000 n=15)
geomean 172.7µ 159.3µ -7.81%
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
LargeMessage-48 4.578Mi ± 0% 4.579Mi ± 7% ~ (p=0.373 n=15)
WriteMemoryUsage-48 41.60Ki ± 0% 41.77Ki ± 0% +0.39% (p=0.000 n=15)
ReadMemoryUsage-48 83.06Ki ± 0% 43.25Ki ± 0% -47.94% (p=0.000 n=15)
geomean 253.0Ki 203.8Ki -19.44%
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
LargeMessage-48 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=15) ¹
WriteMemoryUsage-48 5.000 ± 0% 7.000 ± 0% +40.00% (p=0.000 n=15)
ReadMemoryUsage-48 16.00 ± 0% 18.00 ± 0% +12.50% (p=0.000 n=15)
geomean 5.429 6.316 +16.35%
```
In a [real-world
benchmark](https://github.com/arjan-bal/custom-go-client-benchmark/tree/retry-dp),
where a GCS directpath client downloads a file in a loop, the average
"in use" memory falls from ~35MB to ~20MB.
RELEASE NOTES:
* alts: pool read buffers to lower memory utilization when sockets are
unreadable.diff --git a/credentials/alts/internal/conn/record.go b/credentials/alts/internal/conn/record.go
index c7fcf5e..7fdf349 100644
--- a/credentials/alts/internal/conn/record.go
+++ b/credentials/alts/internal/conn/record.go
@@ -28,6 +28,7 @@
core "google.golang.org/grpc/credentials/alts/internal"
"google.golang.org/grpc/internal/mem"
+ "google.golang.org/grpc/internal/transport"
)
// ALTSRecordCrypto is the interface for gRPC ALTS record protocol.
@@ -75,6 +76,11 @@
var (
protocols = make(map[string]ALTSRecordFunc)
writeBufPool *mem.BinaryTieredBufferPool
+ // readBufPool pools buffers of at least `altsReadBufferInitialSize` size.
+ // Since the read buffer size is slightly larger than 32KB, using a regular
+ // BinaryTieredBufferPool results in allocating buffers of almost double the
+ // required length.
+ readBufPool = mem.NewDirtySimplePool()
)
func init() {
@@ -105,14 +111,18 @@
// conn represents a secured connection. It implements the net.Conn interface.
type conn struct {
net.Conn
+ reader transport.ReadyReader
crypto ALTSRecordCrypto
// buf holds data that has been read from the connection and decrypted,
// but has not yet been returned by Read. It is a sub-slice of protected.
buf []byte
payloadLengthLimit int
- // protected holds data read from the network but have not yet been
- // decrypted. This data might not compose a complete frame.
- protected []byte
+ // protectedHandle buffer holds data read from the network but have not yet
+ // been decrypted. This data might not compose a complete frame.
+ //
+ // The buffer pointer to points to a buffer from the readBufPool. The handle
+ // should only be returned to the pool once nextFrame and buf are empty.
+ protectedHandle *[]byte
// nextFrame stores the next frame (in protected buffer) info.
nextFrame []byte
// overhead is the calculated overhead of each frame.
@@ -135,16 +145,18 @@
// We pre-allocate protected to be of size 32KB during initialization.
// We increase the size of the buffer by the required amount if it can't
// hold a complete encrypted record.
- protectedBuf := make([]byte, max(altsReadBufferInitialSize, len(protected)))
+ protectedHandle := readBufPool.Get(max(altsReadBufferInitialSize, len(protected)))
+ protectedBuf := *protectedHandle
// Copy additional data from hanshaker service.
copy(protectedBuf, protected)
protectedBuf = protectedBuf[:len(protected)]
altsConn := &conn{
Conn: c,
+ reader: transport.NewReadyReader(c),
crypto: crypto,
payloadLengthLimit: payloadLengthLimit,
- protected: protectedBuf,
+ protectedHandle: protectedHandle,
nextFrame: protectedBuf,
overhead: overhead,
}
@@ -158,47 +170,65 @@
func (p *conn) Read(b []byte) (n int, err error) {
if len(p.buf) == 0 {
var framedMsg []byte
+ var protected []byte
+ if p.protectedHandle != nil {
+ protected = *p.protectedHandle
+ protected = protected[:cap(protected)]
+ }
framedMsg, p.nextFrame, err = ParseFramedMsg(p.nextFrame, altsRecordLengthLimit)
if err != nil {
- return n, err
+ return 0, err
}
// Check whether the next frame to be decrypted has been
// completely received yet.
if len(framedMsg) == 0 {
- copy(p.protected, p.nextFrame)
- p.protected = p.protected[:len(p.nextFrame)]
+ copy(protected, p.nextFrame)
+ protected = protected[:len(p.nextFrame)]
// Always copy next incomplete frame to the beginning of
// the protected buffer and reset nextFrame to it.
- p.nextFrame = p.protected
+ p.nextFrame = protected
}
// Check whether a complete frame has been received yet.
for len(framedMsg) == 0 {
- if len(p.protected) == cap(p.protected) {
+ if p.protectedHandle != nil && len(protected) == cap(protected) {
// We can parse the length header to know exactly how large
// the buffer needs to be to hold the entire frame.
- length, didParse := parseMessageLength(p.protected)
+ length, didParse := parseMessageLength(protected)
if !didParse {
// The protected buffer is initialized with a capacity of
// larger than 4B. It should always hold the message length
// header.
- panic(fmt.Sprintf("protected buffer length shorter than expected: %d vs %d", len(p.protected), MsgLenFieldSize))
+ panic(fmt.Sprintf("protected buffer length shorter than expected: %d vs %d", len(protected), MsgLenFieldSize))
}
- oldProtectedBuf := p.protected
+ oldProtectedBuf := protected
+ oldBufHandle := p.protectedHandle
// The new buffer must be able to hold the message length header
// and the entire message.
requiredCapacity := int(length) + MsgLenFieldSize
- p.protected = make([]byte, requiredCapacity)
+ p.protectedHandle = readBufPool.Get(requiredCapacity)
+ protected = *p.protectedHandle
// Copy the contents of the old buffer and set the length of the
// new buffer to the number of bytes already read.
- copy(p.protected, oldProtectedBuf)
- p.protected = p.protected[:len(oldProtectedBuf)]
+ copy(protected, oldProtectedBuf)
+ protected = protected[:len(oldProtectedBuf)]
+ readBufPool.Put(oldBufHandle)
}
- n, err = p.Conn.Read(p.protected[len(p.protected):cap(p.protected)])
- if err != nil {
- return 0, err
+ if p.protectedHandle == nil {
+ // Connection was idle, need to re-allocate the read buffer.
+ newBuf, nRead, err := p.reader.ReadOnReady(altsReadBufferInitialSize, readBufPool)
+ if err != nil {
+ return 0, err
+ }
+ p.protectedHandle = newBuf
+ protected = (*newBuf)[:nRead]
+ } else {
+ nRead, err := p.Conn.Read(protected[len(protected):cap(protected)])
+ if err != nil {
+ return 0, err
+ }
+ protected = protected[:len(protected)+nRead]
}
- p.protected = p.protected[:len(p.protected)+n]
- framedMsg, p.nextFrame, err = ParseFramedMsg(p.protected, altsRecordLengthLimit)
+ framedMsg, p.nextFrame, err = ParseFramedMsg(protected, altsRecordLengthLimit)
if err != nil {
return 0, err
}
@@ -219,6 +249,7 @@
if err != nil {
return 0, err
}
+ p.dropProtectedIfEmtpy()
return len(dec), nil
}
// Decrypt requires that if the dst and ciphertext alias, they
@@ -236,9 +267,23 @@
n = copy(b, p.buf)
p.buf = p.buf[n:]
+ p.dropProtectedIfEmtpy()
return n, nil
}
+func (p *conn) dropProtectedIfEmtpy() {
+ if len(p.buf) > 0 || len(p.nextFrame) > 0 {
+ return
+ }
+ // Potentially idle connection, release the read buffer.
+ p.nextFrame = nil
+ p.buf = nil
+ if p.protectedHandle != nil {
+ readBufPool.Put(p.protectedHandle)
+ p.protectedHandle = nil
+ }
+}
+
// Write encrypts, frames, and writes bytes from b to the underlying connection.
func (p *conn) Write(b []byte) (n int, err error) {
n = len(b)
@@ -257,17 +302,11 @@
writeBuf := *bufHandle
for partialBStart := 0; partialBStart < len(b); partialBStart += partialBSize {
- partialBEnd := partialBStart + partialBSize
- if partialBEnd > len(b) {
- partialBEnd = len(b)
- }
+ partialBEnd := min(partialBStart+partialBSize, len(b))
partialB := b[partialBStart:partialBEnd]
writeBufIndex := 0
for len(partialB) > 0 {
- payloadLen := len(partialB)
- if payloadLen > p.payloadLengthLimit {
- payloadLen = p.payloadLengthLimit
- }
+ payloadLen := min(len(partialB), p.payloadLengthLimit)
buf := partialB[:payloadLen]
partialB = partialB[payloadLen:]
diff --git a/credentials/alts/internal/conn/record_test.go b/credentials/alts/internal/conn/record_test.go
index d9968da..ae5642d 100644
--- a/credentials/alts/internal/conn/record_test.go
+++ b/credentials/alts/internal/conn/record_test.go
@@ -370,16 +370,54 @@
}
}
-// BenchmarkMemoryUsage measures the allocations per ALTS connection.
-// Run this with: go test -bench=BenchmarkMemoryUsage -benchmem
-func BenchmarkMemoryUsage(b *testing.B) {
+// BenchmarkWriteMemoryUsage measures the allocations per ALTS connection.
+// Run this with: go test -bench=BenchmarkWriteMemoryUsage -benchmem
+func BenchmarkWriteMemoryUsage(b *testing.B) {
b.ReportAllocs()
+ data := []byte(strings.Repeat("d", 256))
+ key := []byte{
+ // 16 arbitrary bytes.
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xd2, 0x4c, 0xce, 0x4f, 0x49}
+ conn := &noopConn{}
- for i := 0; i < b.N; i++ {
- c, _ := newConnPair(rekeyRecordProtocol, nil, nil)
+ for b.Loop() {
+ c, err := NewConn(conn, core.ClientSide, rekeyRecordProtocol, key, nil)
+ if err != nil {
+ b.Fatal(err)
+ }
- if _, err := c.Write([]byte("d")); err != nil {
+ if _, err := c.Write(data); err != nil {
b.Fatalf("Write failed: %v", err)
}
}
}
+
+// BenchmarkReadMemoryUsage measures the allocations per ALTS connection.
+// Run this with: go test -bench=BenchmarkReadMemoryUsage -benchmem
+func BenchmarkReadMemoryUsage(b *testing.B) {
+ b.ReportAllocs()
+ data := []byte(strings.Repeat("d", 257))
+ readBuf := make([]byte, 1024)
+
+ for b.Loop() {
+ clientConn, serverConn := newConnPair(rekeyRecordProtocol, nil, nil)
+ b.StopTimer()
+ clientConn.Write(data)
+ b.StartTimer()
+ if _, err := serverConn.Read(readBuf); err != nil {
+ b.Fatalf("Write failed: %v", err)
+ }
+ }
+}
+
+type noopConn struct {
+ net.Conn
+}
+
+func (c *noopConn) Write(b []byte) (n int, err error) {
+ return len(b), nil
+}
+
+func (c *noopConn) Close() error {
+ return nil
+}
diff --git a/internal/mem/buffer_pool.go b/internal/mem/buffer_pool.go
index c2348a8..2d83b2e 100644
--- a/internal/mem/buffer_pool.go
+++ b/internal/mem/buffer_pool.go
@@ -73,7 +73,7 @@
func NewBinaryTieredBufferPool(powerOfTwoExponents ...uint8) (*BinaryTieredBufferPool, error) {
return newBinaryTiered(func(size int) bufferPool {
return newSizedBufferPool(size, true)
- }, &simpleBufferPool{shouldZero: true}, powerOfTwoExponents...)
+ }, &SimpleBufferPool{shouldZero: true}, powerOfTwoExponents...)
}
// NewDirtyBinaryTieredBufferPool returns a BufferPool backed by multiple
@@ -82,7 +82,7 @@
func NewDirtyBinaryTieredBufferPool(powerOfTwoExponents ...uint8) (*BinaryTieredBufferPool, error) {
return newBinaryTiered(func(size int) bufferPool {
return newSizedBufferPool(size, false)
- }, &simpleBufferPool{shouldZero: false}, powerOfTwoExponents...)
+ }, NewDirtySimplePool(), powerOfTwoExponents...)
}
func newBinaryTiered(sizedPoolFactory func(int) bufferPool, fallbackPool bufferPool, powerOfTwoExponents ...uint8) (*BinaryTieredBufferPool, error) {
@@ -258,7 +258,7 @@
// buffer pools for different sizes of buffers.
type TieredBufferPool struct {
sizedPools []*sizedBufferPool
- fallbackPool simpleBufferPool
+ fallbackPool SimpleBufferPool
}
// NewTieredBufferPool returns a BufferPool implementation that uses multiple
@@ -271,7 +271,7 @@
}
return &TieredBufferPool{
sizedPools: pools,
- fallbackPool: simpleBufferPool{shouldZero: true},
+ fallbackPool: SimpleBufferPool{shouldZero: true},
}
}
@@ -297,16 +297,26 @@
return p.sizedPools[poolIdx]
}
-// simpleBufferPool is an implementation of the BufferPool interface that
+// SimpleBufferPool is an implementation of the mem.BufferPool interface that
// attempts to pool buffers with a sync.Pool. When Get is invoked, it tries to
// acquire a buffer from the pool but if that buffer is too small, it returns it
// to the pool and creates a new one.
-type simpleBufferPool struct {
+type SimpleBufferPool struct {
pool sync.Pool
shouldZero bool
}
-func (p *simpleBufferPool) Get(size int) *[]byte {
+// NewDirtySimplePool constructs a [SimpleBufferPool]. It does not initialize
+// the buffers before returning them. Callers must ensure they don't read the
+// buffers before writing data to them.
+func NewDirtySimplePool() *SimpleBufferPool {
+ return &SimpleBufferPool{
+ shouldZero: false,
+ }
+}
+
+// Get returns a buffer with specified length from the pool.
+func (p *SimpleBufferPool) Get(size int) *[]byte {
bs, ok := p.pool.Get().(*[]byte)
if ok && cap(*bs) >= size {
if p.shouldZero {
@@ -333,6 +343,7 @@
return &b
}
-func (p *simpleBufferPool) Put(buf *[]byte) {
+// Put returns a buffer to the pool.
+func (p *SimpleBufferPool) Put(buf *[]byte) {
p.pool.Put(buf)
}
diff --git a/internal/transport/raw_conn_linux.go b/internal/transport/raw_conn_linux.go
new file mode 100644
index 0000000..b897862
--- /dev/null
+++ b/internal/transport/raw_conn_linux.go
@@ -0,0 +1,37 @@
+/*
+ *
+ * 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 transport
+
+import (
+ "golang.org/x/sys/unix"
+)
+
+func isRawConnSupported() bool {
+ return true
+}
+
+// sysRead uses the modern unix package for Unix-like systems.
+func sysRead(fd uintptr, p []byte) (int, error) {
+ return unix.Read(int(fd), p)
+}
+
+// wouldBlock checks standard Unix non-blocking errors.
+func wouldBlock(err error) bool {
+ return err == unix.EAGAIN || err == unix.EWOULDBLOCK
+}
diff --git a/internal/transport/raw_conn_nonlinux.go b/internal/transport/raw_conn_nonlinux.go
new file mode 100644
index 0000000..4edbb9e
--- /dev/null
+++ b/internal/transport/raw_conn_nonlinux.go
@@ -0,0 +1,35 @@
+//go:build !linux
+
+/*
+ *
+ * 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 transport
+
+func isRawConnSupported() bool {
+ return false
+}
+
+// sysRead is not implemented. Support can be added in the future if necessary.
+func sysRead(fd uintptr, p []byte) (int, error) {
+ panic("RawConn functionality is not implemented for non-unix platforms.")
+}
+
+// wouldBlock is not implemented. Support can be added in the future if necessary.
+func wouldBlock(err error) bool {
+ panic("RawConn functionality is not implemented for non-unix platforms.")
+}
diff --git a/internal/transport/ready_reader.go b/internal/transport/ready_reader.go
new file mode 100644
index 0000000..5810bbb
--- /dev/null
+++ b/internal/transport/ready_reader.go
@@ -0,0 +1,105 @@
+/*
+ *
+ * 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 transport
+
+import (
+ "net"
+ "syscall"
+
+ "google.golang.org/grpc/mem"
+)
+
+// ReadyReader is an optional interface that can be implemented by [net.Conn]
+// implementations to enable gRPC to perform non-memory-pinning reads.
+type ReadyReader interface {
+ // ReadOnReady waits for data to arrive, fetches a buffer, and performs a
+ // read. When the underlying IO is readable, it allocates a buffer of size
+ // bufSize from the pool and reads up to bufSize bytes into the buffer.
+ //
+ // It returns a pointer to the buffer so it can be returned to the pool
+ // later, the number of bytes read, and an error.
+ //
+ // Callers should always process the n > 0 bytes returned before considering
+ // the error. Doing so correctly handles I/O errors that happen after
+ // reading some bytes, as well as both of the allowed EOF behaviors.
+ ReadOnReady(bufSize int, pool mem.BufferPool) (b *[]byte, n int, err error)
+}
+
+// nonBlockingReader is optimized for non-memory-pinning reads using the RawConn
+// interface.
+type nonBlockingReader struct {
+ raw syscall.RawConn
+}
+
+func (c *nonBlockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (buf *[]byte, n int, err error) {
+ var readErr error
+ err = c.raw.Read(func(fd uintptr) bool {
+ buf = pool.Get(bufSize)
+ n, readErr = sysRead(fd, *buf)
+ if readErr != nil {
+ pool.Put(buf)
+ buf = nil
+ }
+
+ if wouldBlock(readErr) {
+ return false // Wait for readiness
+ }
+ return true // Done
+ })
+
+ if err != nil {
+ if buf != nil {
+ pool.Put(buf)
+ }
+ return nil, 0, err
+ }
+ if readErr != nil {
+ // buffer is already released in the callback.
+ return nil, 0, readErr
+ }
+ return buf, n, nil
+}
+
+type blockingReader struct {
+ conn net.Conn
+}
+
+func (c *blockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
+ buf := pool.Get(bufSize)
+ n, err := c.conn.Read(*buf)
+ if err != nil {
+ pool.Put(buf)
+ return nil, 0, err
+ }
+ return buf, n, nil
+}
+
+// NewReadyReader detects if [syscall.RawConn] is available for
+// non-memory-pinning reads. If [syscall.RawConn] is unavailable, it falls back
+// to using the simpler [net.Conn] interface for reads.
+func NewReadyReader(conn net.Conn) ReadyReader {
+ sysConn, ok := conn.(syscall.Conn)
+ if !ok || !isRawConnSupported() {
+ return &blockingReader{conn: conn}
+ }
+ if raw, err := sysConn.SyscallConn(); err == nil {
+ return &nonBlockingReader{raw: raw}
+ }
+ return &blockingReader{conn: conn}
+}
diff --git a/internal/transport/ready_reader_test.go b/internal/transport/ready_reader_test.go
new file mode 100644
index 0000000..92efc35
--- /dev/null
+++ b/internal/transport/ready_reader_test.go
@@ -0,0 +1,159 @@
+/*
+ *
+ * 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 transport
+
+import (
+ "bytes"
+ "context"
+ "net"
+ "testing"
+
+ "google.golang.org/grpc/internal/testutils"
+ "google.golang.org/grpc/mem"
+)
+
+// 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 := NewReadyReader(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_TCP_Blocking(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())
+ ac := NewReadyReader(conn)
+
+ resCh := make(chan []byte)
+ const readBufSize = 1024
+
+ go func() {
+ bufHandle, n, err := ac.ReadOnReady(readBufSize, pool)
+ if err != nil {
+ t.Errorf("Failed to read: %v", err)
+ return
+ }
+ defer pool.Put(bufHandle)
+ resCh <- (*bufHandle)[:n]
+ }()
+
+ // 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)
+}