snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (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
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
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (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
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
Stale bot / stale (push) Failing after 1s
This commit is contained in:
141
internal/transport/bdp_estimator.go
Normal file
141
internal/transport/bdp_estimator.go
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2017 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 (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// bdpLimit is the maximum value the flow control windows will be increased
|
||||
// to. TCP typically limits this to 4MB, but some systems go up to 16MB.
|
||||
// Since this is only a limit, it is safe to make it optimistic.
|
||||
bdpLimit = (1 << 20) * 16
|
||||
// alpha is a constant factor used to keep a moving average
|
||||
// of RTTs.
|
||||
alpha = 0.9
|
||||
// If the current bdp sample is greater than or equal to
|
||||
// our beta * our estimated bdp and the current bandwidth
|
||||
// sample is the maximum bandwidth observed so far, we
|
||||
// increase our bbp estimate by a factor of gamma.
|
||||
beta = 0.66
|
||||
// To put our bdp to be smaller than or equal to twice the real BDP,
|
||||
// we should multiply our current sample with 4/3, however to round things out
|
||||
// we use 2 as the multiplication factor.
|
||||
gamma = 2
|
||||
)
|
||||
|
||||
// Adding arbitrary data to ping so that its ack can be identified.
|
||||
// Easter-egg: what does the ping message say?
|
||||
var bdpPing = &ping{data: [8]byte{2, 4, 16, 16, 9, 14, 7, 7}}
|
||||
|
||||
type bdpEstimator struct {
|
||||
// sentAt is the time when the ping was sent.
|
||||
sentAt time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
// bdp is the current bdp estimate.
|
||||
bdp uint32
|
||||
// sample is the number of bytes received in one measurement cycle.
|
||||
sample uint32
|
||||
// bwMax is the maximum bandwidth noted so far (bytes/sec).
|
||||
bwMax float64
|
||||
// bool to keep track of the beginning of a new measurement cycle.
|
||||
isSent bool
|
||||
// Callback to update the window sizes.
|
||||
updateFlowControl func(n uint32)
|
||||
// sampleCount is the number of samples taken so far.
|
||||
sampleCount uint64
|
||||
// round trip time (seconds)
|
||||
rtt float64
|
||||
}
|
||||
|
||||
// timesnap registers the time bdp ping was sent out so that
|
||||
// network rtt can be calculated when its ack is received.
|
||||
// It is called (by controller) when the bdpPing is
|
||||
// being written on the wire.
|
||||
func (b *bdpEstimator) timesnap(d [8]byte) {
|
||||
if bdpPing.data != d {
|
||||
return
|
||||
}
|
||||
b.sentAt = time.Now()
|
||||
}
|
||||
|
||||
// add adds bytes to the current sample for calculating bdp.
|
||||
// It returns true only if a ping must be sent. This can be used
|
||||
// by the caller (handleData) to make decision about batching
|
||||
// a window update with it.
|
||||
func (b *bdpEstimator) add(n uint32) bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bdp == bdpLimit {
|
||||
return false
|
||||
}
|
||||
if !b.isSent {
|
||||
b.isSent = true
|
||||
b.sample = n
|
||||
b.sentAt = time.Time{}
|
||||
b.sampleCount++
|
||||
return true
|
||||
}
|
||||
b.sample += n
|
||||
return false
|
||||
}
|
||||
|
||||
// calculate is called when an ack for a bdp ping is received.
|
||||
// Here we calculate the current bdp and bandwidth sample and
|
||||
// decide if the flow control windows should go up.
|
||||
func (b *bdpEstimator) calculate(d [8]byte) {
|
||||
// Check if the ping acked for was the bdp ping.
|
||||
if bdpPing.data != d {
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
rttSample := time.Since(b.sentAt).Seconds()
|
||||
if b.sampleCount < 10 {
|
||||
// Bootstrap rtt with an average of first 10 rtt samples.
|
||||
b.rtt += (rttSample - b.rtt) / float64(b.sampleCount)
|
||||
} else {
|
||||
// Heed to the recent past more.
|
||||
b.rtt += (rttSample - b.rtt) * float64(alpha)
|
||||
}
|
||||
b.isSent = false
|
||||
// The number of bytes accumulated so far in the sample is smaller
|
||||
// than or equal to 1.5 times the real BDP on a saturated connection.
|
||||
bwCurrent := float64(b.sample) / (b.rtt * float64(1.5))
|
||||
if bwCurrent > b.bwMax {
|
||||
b.bwMax = bwCurrent
|
||||
}
|
||||
// If the current sample (which is smaller than or equal to the 1.5 times the real BDP) is
|
||||
// greater than or equal to 2/3rd our perceived bdp AND this is the maximum bandwidth seen so far, we
|
||||
// should update our perception of the network BDP.
|
||||
if float64(b.sample) >= beta*float64(b.bdp) && bwCurrent == b.bwMax && b.bdp != bdpLimit {
|
||||
sampleFloat := float64(b.sample)
|
||||
b.bdp = uint32(gamma * sampleFloat)
|
||||
if b.bdp > bdpLimit {
|
||||
b.bdp = bdpLimit
|
||||
}
|
||||
bdp := b.bdp
|
||||
b.mu.Unlock()
|
||||
b.updateFlowControl(bdp)
|
||||
return
|
||||
}
|
||||
b.mu.Unlock()
|
||||
}
|
||||
189
internal/transport/client_stream.go
Normal file
189
internal/transport/client_stream.go
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/stats"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// nonGRPCDataMaxLen is the maximum length of nonGRPCDataBuf.
|
||||
//
|
||||
// NOTE: If changed this value, you MUST update the corresponding test in:
|
||||
// - /test/end2end_test.go:TestHTTPServerSendsNonGRPCHeaderSurfaceFurtherData
|
||||
const nonGRPCDataMaxLen = 1024
|
||||
|
||||
// ClientStream implements streaming functionality for a gRPC client.
|
||||
type ClientStream struct {
|
||||
Stream // Embed for common stream functionality.
|
||||
|
||||
ct *http2Client
|
||||
done chan struct{} // closed at the end of stream to unblock writers.
|
||||
|
||||
headerChan chan struct{} // closed to indicate the end of header metadata.
|
||||
header metadata.MD // the received header metadata
|
||||
|
||||
status *status.Status // the status error received from the server
|
||||
|
||||
// Non-pointer fields are at the end to optimize GC allocations.
|
||||
|
||||
// headerValid indicates whether a valid header was received. Only
|
||||
// meaningful after headerChan is closed (always call waitOnHeader() before
|
||||
// reading its value).
|
||||
headerValid bool
|
||||
|
||||
nonGRPCStatus *status.Status // the initial status from the non-gRPC response header, finalized with collected data before closing.
|
||||
nonGRPCDataBuf []byte // stores the data of a non-gRPC response.
|
||||
|
||||
noHeaders bool // set if the client never received headers (set only after the stream is done).
|
||||
headerChanClosed uint32 // set when headerChan is closed. Used to avoid closing headerChan multiple times.
|
||||
bytesReceived atomic.Bool // indicates whether any bytes have been received on this stream
|
||||
unprocessed atomic.Bool // set if the server sends a refused stream or GOAWAY including this stream
|
||||
statsHandler stats.Handler // nil for internal streams (e.g., health check, ORCA) where telemetry is not supported.
|
||||
}
|
||||
|
||||
func (s *ClientStream) startNonGRPCDataCollection(st *status.Status) {
|
||||
s.nonGRPCStatus = st
|
||||
s.nonGRPCDataBuf = make([]byte, 0, nonGRPCDataMaxLen)
|
||||
}
|
||||
|
||||
// finalizeNonGRPCStatus builds the terminal status by appending the collected
|
||||
// response body to the original non-gRPC status message.
|
||||
func (s *ClientStream) finalizeNonGRPCStatus() *status.Status {
|
||||
msg := fmt.Sprintf("%s\ndata: %q", s.nonGRPCStatus.Message(), s.nonGRPCDataBuf)
|
||||
return status.New(s.nonGRPCStatus.Code(), msg)
|
||||
}
|
||||
|
||||
// handleNonGRPCData collects non-gRPC body from the given data frame.
|
||||
// It returns non-nil value when the stream should be closed with it.
|
||||
func (s *ClientStream) handleNonGRPCData(f *parsedDataFrame) *status.Status {
|
||||
n := min(f.data.Len(), nonGRPCDataMaxLen-len(s.nonGRPCDataBuf))
|
||||
s.nonGRPCDataBuf = append(s.nonGRPCDataBuf, f.data.ReadOnlyData()[0:n]...)
|
||||
if len(s.nonGRPCDataBuf) >= nonGRPCDataMaxLen || f.StreamEnded() {
|
||||
return s.finalizeNonGRPCStatus()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads an n byte message from the input stream.
|
||||
func (s *ClientStream) Read(n int) (mem.BufferSlice, error) {
|
||||
b, err := s.Stream.read(n)
|
||||
if err == nil {
|
||||
s.ct.incrMsgRecv()
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Close closes the stream and propagates err to any readers.
|
||||
func (s *ClientStream) Close(err error) {
|
||||
var (
|
||||
rst bool
|
||||
rstCode http2.ErrCode
|
||||
)
|
||||
if err != nil {
|
||||
rst = true
|
||||
rstCode = http2.ErrCodeCancel
|
||||
}
|
||||
s.ct.closeStream(s, err, rst, rstCode, status.Convert(err), nil, false)
|
||||
}
|
||||
|
||||
// Write writes the hdr and data bytes to the output stream.
|
||||
func (s *ClientStream) Write(hdr []byte, data mem.BufferSlice, opts *WriteOptions) error {
|
||||
return s.ct.write(s, hdr, data, opts)
|
||||
}
|
||||
|
||||
// BytesReceived indicates whether any bytes have been received on this stream.
|
||||
func (s *ClientStream) BytesReceived() bool {
|
||||
return s.bytesReceived.Load()
|
||||
}
|
||||
|
||||
// Unprocessed indicates whether the server did not process this stream --
|
||||
// i.e. it sent a refused stream or GOAWAY including this stream ID.
|
||||
func (s *ClientStream) Unprocessed() bool {
|
||||
return s.unprocessed.Load()
|
||||
}
|
||||
|
||||
func (s *ClientStream) waitOnHeader() {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
// Close the stream to prevent headers/trailers from changing after
|
||||
// this function returns.
|
||||
s.Close(ContextErr(s.ctx.Err()))
|
||||
// headerChan could possibly not be closed yet if closeStream raced
|
||||
// with operateHeaders; wait until it is closed explicitly here.
|
||||
<-s.headerChan
|
||||
case <-s.headerChan:
|
||||
}
|
||||
}
|
||||
|
||||
// RecvCompress returns the compression algorithm applied to the inbound
|
||||
// message. It is empty string if there is no compression applied.
|
||||
func (s *ClientStream) RecvCompress() string {
|
||||
s.waitOnHeader()
|
||||
return s.recvCompress
|
||||
}
|
||||
|
||||
// Done returns a channel which is closed when it receives the final status
|
||||
// from the server.
|
||||
func (s *ClientStream) Done() <-chan struct{} {
|
||||
return s.done
|
||||
}
|
||||
|
||||
// Header returns the header metadata of the stream. Acquires the key-value
|
||||
// pairs of header metadata once it is available. It blocks until i) the
|
||||
// metadata is ready or ii) there is no header metadata or iii) the stream is
|
||||
// canceled/expired.
|
||||
func (s *ClientStream) Header() (metadata.MD, error) {
|
||||
s.waitOnHeader()
|
||||
|
||||
if !s.headerValid || s.noHeaders {
|
||||
return nil, s.status.Err()
|
||||
}
|
||||
|
||||
return s.header.Copy(), nil
|
||||
}
|
||||
|
||||
// TrailersOnly blocks until a header or trailers-only frame is received and
|
||||
// then returns true if the stream was trailers-only. If the stream ends
|
||||
// before headers are received, returns true, nil.
|
||||
func (s *ClientStream) TrailersOnly() bool {
|
||||
s.waitOnHeader()
|
||||
return s.noHeaders
|
||||
}
|
||||
|
||||
// Status returns the status received from the server.
|
||||
// Status can be read safely only after the stream has ended,
|
||||
// that is, after Done() is closed.
|
||||
func (s *ClientStream) Status() *status.Status {
|
||||
return s.status
|
||||
}
|
||||
|
||||
func (s *ClientStream) requestRead(n int) {
|
||||
s.ct.adjustWindow(s, uint32(n))
|
||||
}
|
||||
|
||||
func (s *ClientStream) updateWindow(n int) {
|
||||
s.ct.updateWindow(s, uint32(n))
|
||||
}
|
||||
1051
internal/transport/controlbuf.go
Normal file
1051
internal/transport/controlbuf.go
Normal file
File diff suppressed because it is too large
Load Diff
85
internal/transport/controlbuf_test.go
Normal file
85
internal/transport/controlbuf_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
*
|
||||
* 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 (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s) TestControlBuffer_Throttle(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
cb := newControlBuffer(done)
|
||||
|
||||
// Fill the control buffer up to the limit with throttled items.
|
||||
for i := 0; i < maxQueuedControlBufferItems; i++ {
|
||||
cb.put(&ping{})
|
||||
}
|
||||
|
||||
// The next call to throttle should block.
|
||||
throttleDone := make(chan struct{})
|
||||
go func() {
|
||||
cb.throttle()
|
||||
close(throttleDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-throttleDone:
|
||||
t.Fatal("throttle() did not block when the buffer was full")
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Consume one item from the control buffer.
|
||||
if _, err := cb.get(true); err != nil {
|
||||
t.Fatalf("cb.get(true) failed: %v", err)
|
||||
}
|
||||
|
||||
// Now throttle() should unblock.
|
||||
select {
|
||||
case <-throttleDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("throttle() did not unblock after an item was consumed")
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestControlBuffer_NoThrottleForNonThrottledItems(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
cb := newControlBuffer(done)
|
||||
|
||||
// Fill the control buffer with many more than limit number of non-throttled
|
||||
// items.
|
||||
for i := 0; i < maxQueuedControlBufferItems+10; i++ {
|
||||
cb.put(&dataFrame{})
|
||||
}
|
||||
|
||||
// throttle() should not block.
|
||||
throttled := make(chan struct{})
|
||||
go func() {
|
||||
cb.throttle()
|
||||
close(throttled)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-throttled:
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
t.Fatal("throttle() blocked for non-throttled items")
|
||||
}
|
||||
}
|
||||
56
internal/transport/defaults.go
Normal file
56
internal/transport/defaults.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2018 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 (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// The default value of flow control window size in HTTP2 spec.
|
||||
defaultWindowSize = 65535
|
||||
// The initial window size for flow control.
|
||||
initialWindowSize = defaultWindowSize // for an RPC
|
||||
infinity = time.Duration(math.MaxInt64)
|
||||
defaultClientKeepaliveTime = infinity
|
||||
defaultClientKeepaliveTimeout = 20 * time.Second
|
||||
defaultMaxStreamsClient = 100
|
||||
defaultMaxConnectionIdle = infinity
|
||||
defaultMaxConnectionAge = infinity
|
||||
defaultMaxConnectionAgeGrace = infinity
|
||||
defaultServerKeepaliveTime = 2 * time.Hour
|
||||
defaultServerKeepaliveTimeout = 20 * time.Second
|
||||
defaultKeepalivePolicyMinTime = 5 * time.Minute
|
||||
// max window limit set by HTTP2 Specs.
|
||||
maxWindowSize = math.MaxInt32
|
||||
// defaultWriteQuota is the default value for number of data
|
||||
// bytes that each stream can schedule before some of it being
|
||||
// flushed out.
|
||||
defaultWriteQuota = 64 * 1024
|
||||
defaultClientMaxHeaderListSize = uint32(16 << 20)
|
||||
defaultServerMaxHeaderListSize = uint32(16 << 20)
|
||||
upcomingDefaultHeaderListSize = uint32(8 << 10)
|
||||
)
|
||||
|
||||
// MaxStreamID is the upper bound for the stream ID before the current
|
||||
// transport gracefully closes and new transport is created for subsequent RPCs.
|
||||
// This is set to 75% of 2^31-1. Streams are identified with an unsigned 31-bit
|
||||
// integer. It's exported so that tests can override it.
|
||||
var MaxStreamID = uint32(math.MaxInt32 * 3 / 4)
|
||||
211
internal/transport/flowcontrol.go
Normal file
211
internal/transport/flowcontrol.go
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2014 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 (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// writeQuota is a soft limit on the amount of data a stream can
|
||||
// schedule before some of it is written out.
|
||||
type writeQuota struct {
|
||||
_ noCopy
|
||||
// get waits on read from when quota goes less than or equal to zero.
|
||||
// replenish writes on it when quota goes positive again.
|
||||
ch chan struct{}
|
||||
// done is triggered in error case.
|
||||
done <-chan struct{}
|
||||
// replenish is called by loopyWriter to give quota back to.
|
||||
// It is implemented as a field so that it can be updated
|
||||
// by tests.
|
||||
replenish func(n int)
|
||||
quota int32
|
||||
}
|
||||
|
||||
// init allows a writeQuota to be initialized in-place, which is useful for
|
||||
// resetting a buffer or for avoiding a heap allocation when the buffer is
|
||||
// embedded in another struct.
|
||||
func (w *writeQuota) init(sz int32, done <-chan struct{}) {
|
||||
w.quota = sz
|
||||
w.ch = make(chan struct{}, 1)
|
||||
w.done = done
|
||||
w.replenish = w.realReplenish
|
||||
}
|
||||
|
||||
func (w *writeQuota) get(sz int32) error {
|
||||
for {
|
||||
if atomic.LoadInt32(&w.quota) > 0 {
|
||||
atomic.AddInt32(&w.quota, -sz)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-w.ch:
|
||||
continue
|
||||
case <-w.done:
|
||||
return errStreamDone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *writeQuota) realReplenish(n int) {
|
||||
sz := int32(n)
|
||||
newQuota := atomic.AddInt32(&w.quota, sz)
|
||||
previousQuota := newQuota - sz
|
||||
if previousQuota <= 0 && newQuota > 0 {
|
||||
select {
|
||||
case w.ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type trInFlow struct {
|
||||
limit uint32
|
||||
unacked uint32
|
||||
effectiveWindowSize uint32
|
||||
}
|
||||
|
||||
func (f *trInFlow) newLimit(n uint32) uint32 {
|
||||
d := n - f.limit
|
||||
f.limit = n
|
||||
f.updateEffectiveWindowSize()
|
||||
return d
|
||||
}
|
||||
|
||||
func (f *trInFlow) onData(n uint32) uint32 {
|
||||
f.unacked += n
|
||||
if f.unacked < f.limit/4 {
|
||||
f.updateEffectiveWindowSize()
|
||||
return 0
|
||||
}
|
||||
return f.reset()
|
||||
}
|
||||
|
||||
func (f *trInFlow) reset() uint32 {
|
||||
w := f.unacked
|
||||
f.unacked = 0
|
||||
f.updateEffectiveWindowSize()
|
||||
return w
|
||||
}
|
||||
|
||||
func (f *trInFlow) updateEffectiveWindowSize() {
|
||||
atomic.StoreUint32(&f.effectiveWindowSize, f.limit-f.unacked)
|
||||
}
|
||||
|
||||
func (f *trInFlow) getSize() uint32 {
|
||||
return atomic.LoadUint32(&f.effectiveWindowSize)
|
||||
}
|
||||
|
||||
// inFlow deals with inbound flow control
|
||||
type inFlow struct {
|
||||
mu sync.Mutex
|
||||
// The inbound flow control limit for pending data.
|
||||
limit uint32
|
||||
// pendingData is the overall data which have been received but not been
|
||||
// consumed by applications.
|
||||
pendingData uint32
|
||||
// The amount of data the application has consumed but grpc has not sent
|
||||
// window update for them. Used to reduce window update frequency.
|
||||
pendingUpdate uint32
|
||||
// delta is the extra window update given by receiver when an application
|
||||
// is reading data bigger in size than the inFlow limit.
|
||||
delta uint32
|
||||
}
|
||||
|
||||
// newLimit updates the inflow window to a new value n.
|
||||
// It assumes that n is always greater than the old limit.
|
||||
func (f *inFlow) newLimit(n uint32) {
|
||||
f.mu.Lock()
|
||||
f.limit = n
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *inFlow) maybeAdjust(n uint32) uint32 {
|
||||
if n > uint32(math.MaxInt32) {
|
||||
n = uint32(math.MaxInt32)
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
// estSenderQuota is the receiver's view of the maximum number of bytes the sender
|
||||
// can send without a window update.
|
||||
estSenderQuota := int32(f.limit - (f.pendingData + f.pendingUpdate))
|
||||
// estUntransmittedData is the maximum number of bytes the sends might not have put
|
||||
// on the wire yet. A value of 0 or less means that we have already received all or
|
||||
// more bytes than the application is requesting to read.
|
||||
estUntransmittedData := int32(n - f.pendingData) // Casting into int32 since it could be negative.
|
||||
// This implies that unless we send a window update, the sender won't be able to send all the bytes
|
||||
// for this message. Therefore we must send an update over the limit since there's an active read
|
||||
// request from the application.
|
||||
if estUntransmittedData > estSenderQuota {
|
||||
// Sender's window shouldn't go more than 2^31 - 1 as specified in the HTTP spec.
|
||||
if f.limit+n > maxWindowSize {
|
||||
f.delta = maxWindowSize - f.limit
|
||||
} else {
|
||||
// Send a window update for the whole message and not just the difference between
|
||||
// estUntransmittedData and estSenderQuota. This will be helpful in case the message
|
||||
// is padded; We will fallback on the current available window(at least a 1/4th of the limit).
|
||||
f.delta = n
|
||||
}
|
||||
return f.delta
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// onData is invoked when some data frame is received. It updates pendingData.
|
||||
func (f *inFlow) onData(n uint32) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
f.pendingData += n
|
||||
if f.pendingData+f.pendingUpdate > f.limit+f.delta {
|
||||
limit := f.limit
|
||||
rcvd := f.pendingData + f.pendingUpdate
|
||||
return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", rcvd, limit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onRead is invoked when the application reads the data. It returns the window size
|
||||
// to be sent to the peer.
|
||||
func (f *inFlow) onRead(n uint32) uint32 {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if f.pendingData == 0 {
|
||||
return 0
|
||||
}
|
||||
f.pendingData -= n
|
||||
if n > f.delta {
|
||||
n -= f.delta
|
||||
f.delta = 0
|
||||
} else {
|
||||
f.delta -= n
|
||||
n = 0
|
||||
}
|
||||
f.pendingUpdate += n
|
||||
if f.pendingUpdate >= f.limit/4 {
|
||||
wu := f.pendingUpdate
|
||||
f.pendingUpdate = 0
|
||||
return wu
|
||||
}
|
||||
return 0
|
||||
}
|
||||
506
internal/transport/handler_server.go
Normal file
506
internal/transport/handler_server.go
Normal file
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2016 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// This file is the implementation of a gRPC server using HTTP/2 which
|
||||
// uses the standard Go http2 Server implementation (via the
|
||||
// http.Handler interface), rather than speaking low-level HTTP/2
|
||||
// frames itself. It is the implementation of *grpc.Server.ServeHTTP.
|
||||
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/grpcutil"
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/stats"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// NewServerHandlerTransport returns a ServerTransport handling gRPC from
|
||||
// inside an http.Handler, or writes an HTTP error to w and returns an error.
|
||||
// It requires that the http Server supports HTTP/2.
|
||||
func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats stats.Handler, bufferPool mem.BufferPool) (ServerTransport, error) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
msg := fmt.Sprintf("invalid gRPC request method %q", r.Method)
|
||||
http.Error(w, msg, http.StatusMethodNotAllowed)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
// TODO: do we assume contentType is lowercase? we did before
|
||||
contentSubtype, validContentType := grpcutil.ContentSubtype(contentType)
|
||||
if !validContentType {
|
||||
msg := fmt.Sprintf("invalid gRPC request content-type %q", contentType)
|
||||
http.Error(w, msg, http.StatusUnsupportedMediaType)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if r.ProtoMajor != 2 {
|
||||
msg := "gRPC requires HTTP/2"
|
||||
http.Error(w, msg, http.StatusHTTPVersionNotSupported)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
if _, ok := w.(http.Flusher); !ok {
|
||||
msg := "gRPC requires a ResponseWriter supporting http.Flusher"
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
var localAddr net.Addr
|
||||
if la := r.Context().Value(http.LocalAddrContextKey); la != nil {
|
||||
localAddr, _ = la.(net.Addr)
|
||||
}
|
||||
var authInfo credentials.AuthInfo
|
||||
if r.TLS != nil {
|
||||
authInfo = credentials.TLSInfo{State: *r.TLS, CommonAuthInfo: credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}
|
||||
}
|
||||
p := peer.Peer{
|
||||
Addr: strAddr(r.RemoteAddr),
|
||||
LocalAddr: localAddr,
|
||||
AuthInfo: authInfo,
|
||||
}
|
||||
st := &serverHandlerTransport{
|
||||
rw: w,
|
||||
req: r,
|
||||
closedCh: make(chan struct{}),
|
||||
writes: make(chan func()),
|
||||
peer: p,
|
||||
contentType: contentType,
|
||||
contentSubtype: contentSubtype,
|
||||
stats: stats,
|
||||
bufferPool: bufferPool,
|
||||
}
|
||||
st.logger = prefixLoggerForServerHandlerTransport(st)
|
||||
|
||||
if v := r.Header.Get("grpc-timeout"); v != "" {
|
||||
to, err := decodeTimeout(v)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("malformed grpc-timeout: %v", err)
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return nil, status.Error(codes.Internal, msg)
|
||||
}
|
||||
st.timeoutSet = true
|
||||
st.timeout = to
|
||||
}
|
||||
|
||||
metakv := []string{"content-type", contentType}
|
||||
if r.Host != "" {
|
||||
metakv = append(metakv, ":authority", r.Host)
|
||||
}
|
||||
for k, vv := range r.Header {
|
||||
k = strings.ToLower(k)
|
||||
if isReservedHeader(k) && !isWhitelistedHeader(k) {
|
||||
continue
|
||||
}
|
||||
for _, v := range vv {
|
||||
v, err := decodeMetadataHeader(k, v)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("malformed binary metadata %q in header %q: %v", v, k, err)
|
||||
http.Error(w, msg, http.StatusBadRequest)
|
||||
return nil, status.Error(codes.Internal, msg)
|
||||
}
|
||||
metakv = append(metakv, k, v)
|
||||
}
|
||||
}
|
||||
st.headerMD = metadata.Pairs(metakv...)
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// serverHandlerTransport is an implementation of ServerTransport
|
||||
// which replies to exactly one gRPC request (exactly one HTTP request),
|
||||
// using the net/http.Handler interface. This http.Handler is guaranteed
|
||||
// at this point to be speaking over HTTP/2, so it's able to speak valid
|
||||
// gRPC.
|
||||
type serverHandlerTransport struct {
|
||||
rw http.ResponseWriter
|
||||
req *http.Request
|
||||
timeoutSet bool
|
||||
timeout time.Duration
|
||||
|
||||
headerMD metadata.MD
|
||||
|
||||
peer peer.Peer
|
||||
|
||||
closeOnce sync.Once
|
||||
closedCh chan struct{} // closed on Close
|
||||
|
||||
// writes is a channel of code to run serialized in the
|
||||
// ServeHTTP (HandleStreams) goroutine. The channel is closed
|
||||
// when WriteStatus is called.
|
||||
writes chan func()
|
||||
|
||||
// block concurrent WriteStatus calls
|
||||
// e.g. grpc/(*serverStream).SendMsg/RecvMsg
|
||||
writeStatusMu sync.Mutex
|
||||
|
||||
// we just mirror the request content-type
|
||||
contentType string
|
||||
// we store both contentType and contentSubtype so we don't keep recreating them
|
||||
// TODO make sure this is consistent across handler_server and http2_server
|
||||
contentSubtype string
|
||||
|
||||
stats stats.Handler
|
||||
logger *grpclog.PrefixLogger
|
||||
|
||||
bufferPool mem.BufferPool
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) Close(err error) {
|
||||
ht.closeOnce.Do(func() {
|
||||
if ht.logger.V(logLevel) {
|
||||
ht.logger.Infof("Closing: %v", err)
|
||||
}
|
||||
close(ht.closedCh)
|
||||
})
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) Peer() *peer.Peer {
|
||||
return &peer.Peer{
|
||||
Addr: ht.peer.Addr,
|
||||
LocalAddr: ht.peer.LocalAddr,
|
||||
AuthInfo: ht.peer.AuthInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// strAddr is a net.Addr backed by either a TCP "ip:port" string, or
|
||||
// the empty string if unknown.
|
||||
type strAddr string
|
||||
|
||||
func (a strAddr) Network() string {
|
||||
if a != "" {
|
||||
// Per the documentation on net/http.Request.RemoteAddr, if this is
|
||||
// set, it's set to the IP:port of the peer (hence, TCP):
|
||||
// https://golang.org/pkg/net/http/#Request
|
||||
//
|
||||
// If we want to support Unix sockets later, we can
|
||||
// add our own grpc-specific convention within the
|
||||
// grpc codebase to set RemoteAddr to a different
|
||||
// format, or probably better: we can attach it to the
|
||||
// context and use that from serverHandlerTransport.RemoteAddr.
|
||||
return "tcp"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a strAddr) String() string { return string(a) }
|
||||
|
||||
// do runs fn in the ServeHTTP goroutine.
|
||||
func (ht *serverHandlerTransport) do(fn func()) error {
|
||||
select {
|
||||
case <-ht.closedCh:
|
||||
return ErrConnClosing
|
||||
case ht.writes <- fn:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) writeStatus(s *ServerStream, st *status.Status) error {
|
||||
ht.writeStatusMu.Lock()
|
||||
defer ht.writeStatusMu.Unlock()
|
||||
|
||||
headersWritten := s.updateHeaderSent()
|
||||
err := ht.do(func() {
|
||||
if !headersWritten {
|
||||
ht.writePendingHeaders(s)
|
||||
}
|
||||
|
||||
// And flush, in case no header or body has been sent yet.
|
||||
// This forces a separation of headers and trailers if this is the
|
||||
// first call (for example, in end2end tests's TestNoService).
|
||||
ht.rw.(http.Flusher).Flush()
|
||||
|
||||
h := ht.rw.Header()
|
||||
h.Set("Grpc-Status", fmt.Sprintf("%d", st.Code()))
|
||||
if m := st.Message(); m != "" {
|
||||
h.Set("Grpc-Message", encodeGrpcMessage(m))
|
||||
}
|
||||
|
||||
s.hdrMu.Lock()
|
||||
defer s.hdrMu.Unlock()
|
||||
if p := st.Proto(); p != nil && len(p.Details) > 0 {
|
||||
delete(s.trailer, grpcStatusDetailsBinHeader)
|
||||
stBytes, err := proto.Marshal(p)
|
||||
if err != nil {
|
||||
// TODO: return error instead, when callers are able to handle it.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
h.Set(grpcStatusDetailsBinHeader, encodeBinHeader(stBytes))
|
||||
}
|
||||
|
||||
if len(s.trailer) > 0 {
|
||||
for k, vv := range s.trailer {
|
||||
// Clients don't tolerate reading restricted headers after some non restricted ones were sent.
|
||||
if isReservedHeader(k) {
|
||||
continue
|
||||
}
|
||||
for _, v := range vv {
|
||||
// http2 ResponseWriter mechanism to send undeclared Trailers after
|
||||
// the headers have possibly been written.
|
||||
h.Add(http2.TrailerPrefix+k, encodeMetadataHeader(k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err == nil && ht.stats != nil { // transport has not been closed
|
||||
// Note: The trailer fields are compressed with hpack after this call returns.
|
||||
// No WireLength field is set here.
|
||||
s.hdrMu.Lock()
|
||||
ht.stats.HandleRPC(s.Context(), &stats.OutTrailer{
|
||||
Trailer: s.trailer.Copy(),
|
||||
})
|
||||
s.hdrMu.Unlock()
|
||||
}
|
||||
ht.Close(errors.New("finished writing status"))
|
||||
return err
|
||||
}
|
||||
|
||||
// writePendingHeaders sets common and custom headers on the first
|
||||
// write call (Write, WriteHeader, or WriteStatus)
|
||||
func (ht *serverHandlerTransport) writePendingHeaders(s *ServerStream) {
|
||||
ht.writeCommonHeaders(s)
|
||||
ht.writeCustomHeaders(s)
|
||||
}
|
||||
|
||||
// writeCommonHeaders sets common headers on the first write
|
||||
// call (Write, WriteHeader, or WriteStatus).
|
||||
func (ht *serverHandlerTransport) writeCommonHeaders(s *ServerStream) {
|
||||
h := ht.rw.Header()
|
||||
h["Date"] = nil // suppress Date to make tests happy; TODO: restore
|
||||
h.Set("Content-Type", ht.contentType)
|
||||
|
||||
// Predeclare trailers we'll set later in WriteStatus (after the body).
|
||||
// This is a SHOULD in the HTTP RFC, and the way you add (known)
|
||||
// Trailers per the net/http.ResponseWriter contract.
|
||||
// See https://golang.org/pkg/net/http/#ResponseWriter
|
||||
// and https://golang.org/pkg/net/http/#example_ResponseWriter_trailers
|
||||
h.Add("Trailer", "Grpc-Status")
|
||||
h.Add("Trailer", "Grpc-Message")
|
||||
h.Add("Trailer", "Grpc-Status-Details-Bin")
|
||||
|
||||
if s.sendCompress != "" {
|
||||
h.Set("Grpc-Encoding", s.sendCompress)
|
||||
}
|
||||
}
|
||||
|
||||
// writeCustomHeaders sets custom headers set on the stream via SetHeader
|
||||
// on the first write call (Write, WriteHeader, or WriteStatus)
|
||||
func (ht *serverHandlerTransport) writeCustomHeaders(s *ServerStream) {
|
||||
h := ht.rw.Header()
|
||||
|
||||
s.hdrMu.Lock()
|
||||
for k, vv := range s.header {
|
||||
if isReservedHeader(k) {
|
||||
continue
|
||||
}
|
||||
for _, v := range vv {
|
||||
h.Add(k, encodeMetadataHeader(k, v))
|
||||
}
|
||||
}
|
||||
|
||||
s.hdrMu.Unlock()
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) write(s *ServerStream, hdr []byte, data mem.BufferSlice, _ *WriteOptions) error {
|
||||
// Always take a reference because otherwise there is no guarantee the data will
|
||||
// be available after this function returns. This is what callers to Write
|
||||
// expect.
|
||||
data.Ref()
|
||||
headersWritten := s.updateHeaderSent()
|
||||
err := ht.do(func() {
|
||||
defer data.Free()
|
||||
if !headersWritten {
|
||||
ht.writePendingHeaders(s)
|
||||
}
|
||||
ht.rw.Write(hdr)
|
||||
for _, b := range data {
|
||||
_, _ = ht.rw.Write(b.ReadOnlyData())
|
||||
}
|
||||
ht.rw.(http.Flusher).Flush()
|
||||
})
|
||||
if err != nil {
|
||||
data.Free()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) writeHeader(s *ServerStream, md metadata.MD) error {
|
||||
if err := s.SetHeader(md); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headersWritten := s.updateHeaderSent()
|
||||
err := ht.do(func() {
|
||||
if !headersWritten {
|
||||
ht.writePendingHeaders(s)
|
||||
}
|
||||
|
||||
ht.rw.WriteHeader(200)
|
||||
ht.rw.(http.Flusher).Flush()
|
||||
})
|
||||
|
||||
if err == nil && ht.stats != nil {
|
||||
// Note: The header fields are compressed with hpack after this call returns.
|
||||
// No WireLength field is set here.
|
||||
ht.stats.HandleRPC(s.Context(), &stats.OutHeader{
|
||||
Header: md.Copy(),
|
||||
Compression: s.sendCompress,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) adjustWindow(*ServerStream, uint32) {
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) updateWindow(*ServerStream, uint32) {
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) HandleStreams(ctx context.Context, startStream func(*ServerStream)) {
|
||||
// With this transport type there will be exactly 1 stream: this HTTP request.
|
||||
var cancel context.CancelFunc
|
||||
if ht.timeoutSet {
|
||||
ctx, cancel = context.WithTimeout(ctx, ht.timeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
|
||||
// requestOver is closed when the status has been written via WriteStatus.
|
||||
requestOver := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-requestOver:
|
||||
case <-ht.closedCh:
|
||||
case <-ht.req.Context().Done():
|
||||
}
|
||||
cancel()
|
||||
ht.Close(errors.New("request is done processing"))
|
||||
}()
|
||||
|
||||
ctx = metadata.NewIncomingContext(ctx, ht.headerMD)
|
||||
req := ht.req
|
||||
s := &ServerStream{
|
||||
Stream: Stream{
|
||||
id: 0, // irrelevant
|
||||
ctx: ctx,
|
||||
method: req.URL.Path,
|
||||
recvCompress: req.Header.Get("grpc-encoding"),
|
||||
contentSubtype: ht.contentSubtype,
|
||||
},
|
||||
cancel: cancel,
|
||||
st: ht,
|
||||
headerWireLength: 0, // won't have access to header wire length until golang/go#18997.
|
||||
}
|
||||
s.Stream.buf.init(ht.bufferPool)
|
||||
s.readRequester = s
|
||||
s.trReader = transportReader{
|
||||
reader: recvBufferReader{ctx: s.ctx, ctxDone: s.ctx.Done(), recv: &s.buf},
|
||||
windowHandler: s,
|
||||
}
|
||||
|
||||
// readerDone is closed when the Body.Read-ing goroutine exits.
|
||||
readerDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readerDone)
|
||||
|
||||
for {
|
||||
buf := ht.bufferPool.Get(http2MaxFrameLen)
|
||||
n, err := req.Body.Read(*buf)
|
||||
if n > 0 {
|
||||
*buf = (*buf)[:n]
|
||||
s.buf.put(recvMsg{buffer: mem.NewBuffer(buf, ht.bufferPool)})
|
||||
} else {
|
||||
ht.bufferPool.Put(buf)
|
||||
}
|
||||
if err != nil {
|
||||
s.buf.put(recvMsg{err: mapRecvMsgError(err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// startStream is provided by the *grpc.Server's serveStreams.
|
||||
// It starts a goroutine serving s and exits immediately.
|
||||
// The goroutine that is started is the one that then calls
|
||||
// into ht, calling WriteHeader, Write, WriteStatus, Close, etc.
|
||||
startStream(s)
|
||||
|
||||
ht.runStream()
|
||||
close(requestOver)
|
||||
|
||||
// Wait for reading goroutine to finish.
|
||||
req.Body.Close()
|
||||
<-readerDone
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) runStream() {
|
||||
for {
|
||||
select {
|
||||
case fn := <-ht.writes:
|
||||
fn()
|
||||
case <-ht.closedCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ht *serverHandlerTransport) incrMsgRecv() {}
|
||||
|
||||
func (ht *serverHandlerTransport) Drain(s string) {
|
||||
ht.Close(errors.New(s))
|
||||
}
|
||||
|
||||
// mapRecvMsgError returns the non-nil err into the appropriate
|
||||
// error value as expected by callers of *grpc.parser.recvMsg.
|
||||
// In particular, in can only be:
|
||||
// - io.EOF
|
||||
// - io.ErrUnexpectedEOF
|
||||
// - of type transport.ConnectionError
|
||||
// - an error from the status package
|
||||
func mapRecvMsgError(err error) error {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
return err
|
||||
}
|
||||
if se, ok := err.(http2.StreamError); ok {
|
||||
if code, ok := http2ErrConvTab[se.Code]; ok {
|
||||
return status.Error(code, se.Error())
|
||||
}
|
||||
}
|
||||
if strings.Contains(err.Error(), "body closed by handler") {
|
||||
return status.Error(codes.Canceled, err.Error())
|
||||
}
|
||||
return connectionErrorf(true, err, "%s", err.Error())
|
||||
}
|
||||
614
internal/transport/handler_server_test.go
Normal file
614
internal/transport/handler_server_test.go
Normal file
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2016 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
epb "google.golang.org/genproto/googleapis/rpc/errdetails"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/stats"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/protoadapt"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
func (s) TestHandlerTransport_NewServerHandlerTransport(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
req *http.Request
|
||||
wantErr string
|
||||
wantErrCode int
|
||||
modrw func(http.ResponseWriter) http.ResponseWriter
|
||||
check func(*serverHandlerTransport, *testCase) error
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "bad method",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "GET",
|
||||
Header: http.Header{},
|
||||
},
|
||||
wantErr: `invalid gRPC request method "GET"`,
|
||||
wantErrCode: http.StatusMethodNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "bad content type",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/foo"},
|
||||
},
|
||||
},
|
||||
wantErr: `invalid gRPC request content-type "application/foo"`,
|
||||
wantErrCode: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
name: "http/1.1",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Method: "POST",
|
||||
Header: http.Header{"Content-Type": []string{"application/grpc"}},
|
||||
},
|
||||
wantErr: "gRPC requires HTTP/2",
|
||||
wantErrCode: http.StatusHTTPVersionNotSupported,
|
||||
},
|
||||
{
|
||||
name: "not flusher",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/grpc"},
|
||||
},
|
||||
},
|
||||
modrw: func(w http.ResponseWriter) http.ResponseWriter {
|
||||
// Return w without its Flush method
|
||||
type onlyCloseNotifier interface {
|
||||
http.ResponseWriter
|
||||
}
|
||||
return struct{ onlyCloseNotifier }{w.(onlyCloseNotifier)}
|
||||
},
|
||||
wantErr: "gRPC requires a ResponseWriter supporting http.Flusher",
|
||||
wantErrCode: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/grpc"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
},
|
||||
check: func(t *serverHandlerTransport, tt *testCase) error {
|
||||
if t.req != tt.req {
|
||||
return fmt.Errorf("t.req = %p; want %p", t.req, tt.req)
|
||||
}
|
||||
if t.rw == nil {
|
||||
return errors.New("t.rw = nil; want non-nil")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with timeout",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/grpc"},
|
||||
"Grpc-Timeout": {"200m"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
},
|
||||
check: func(t *serverHandlerTransport, _ *testCase) error {
|
||||
if !t.timeoutSet {
|
||||
return errors.New("timeout not set")
|
||||
}
|
||||
if want := 200 * time.Millisecond; t.timeout != want {
|
||||
return fmt.Errorf("timeout = %v; want %v", t.timeout, want)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with bad timeout",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/grpc"},
|
||||
"Grpc-Timeout": {"tomorrow"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
},
|
||||
wantErr: `rpc error: code = Internal desc = malformed grpc-timeout: transport: timeout unit is not recognized: "tomorrow"`,
|
||||
wantErrCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "with metadata",
|
||||
req: &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/grpc"},
|
||||
"meta-foo": {"foo-val"},
|
||||
"meta-bar": {"bar-val1", "bar-val2"},
|
||||
"user-agent": {"x/y a/b"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
},
|
||||
check: func(ht *serverHandlerTransport, _ *testCase) error {
|
||||
want := metadata.MD{
|
||||
"meta-bar": {"bar-val1", "bar-val2"},
|
||||
"user-agent": {"x/y a/b"},
|
||||
"meta-foo": {"foo-val"},
|
||||
"content-type": {"application/grpc"},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ht.headerMD, want) {
|
||||
return fmt.Errorf("metadata = %#v; want %#v", ht.headerMD, want)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
rrec := httptest.NewRecorder()
|
||||
rw := http.ResponseWriter(testHandlerResponseWriter{
|
||||
ResponseRecorder: rrec,
|
||||
})
|
||||
|
||||
if tt.modrw != nil {
|
||||
rw = tt.modrw(rw)
|
||||
}
|
||||
got, gotErr := NewServerHandlerTransport(rw, tt.req, nil, mem.DefaultBufferPool())
|
||||
if (gotErr != nil) != (tt.wantErr != "") || (gotErr != nil && gotErr.Error() != tt.wantErr) {
|
||||
t.Errorf("%s: error = %q; want %q", tt.name, gotErr.Error(), tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if tt.wantErrCode == 0 {
|
||||
tt.wantErrCode = http.StatusOK
|
||||
}
|
||||
if rrec.Code != tt.wantErrCode {
|
||||
t.Errorf("%s: code = %d; want %d", tt.name, rrec.Code, tt.wantErrCode)
|
||||
continue
|
||||
}
|
||||
if gotErr != nil {
|
||||
continue
|
||||
}
|
||||
if tt.check != nil {
|
||||
if err := tt.check(got.(*serverHandlerTransport), &tt); err != nil {
|
||||
t.Errorf("%s: %v", tt.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testHandlerResponseWriter struct {
|
||||
*httptest.ResponseRecorder
|
||||
}
|
||||
|
||||
func (w testHandlerResponseWriter) Flush() {}
|
||||
|
||||
func newTestHandlerResponseWriter() http.ResponseWriter {
|
||||
return testHandlerResponseWriter{
|
||||
ResponseRecorder: httptest.NewRecorder(),
|
||||
}
|
||||
}
|
||||
|
||||
type handleStreamTest struct {
|
||||
t *testing.T
|
||||
bodyw *io.PipeWriter
|
||||
rw testHandlerResponseWriter
|
||||
ht *serverHandlerTransport
|
||||
}
|
||||
|
||||
type mockStatsHandler struct {
|
||||
rpcStatsCh chan stats.RPCStats
|
||||
}
|
||||
|
||||
func (h *mockStatsHandler) TagRPC(ctx context.Context, _ *stats.RPCTagInfo) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (h *mockStatsHandler) HandleRPC(_ context.Context, s stats.RPCStats) {
|
||||
h.rpcStatsCh <- s
|
||||
}
|
||||
|
||||
func (h *mockStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (h *mockStatsHandler) HandleConn(context.Context, stats.ConnStats) {
|
||||
}
|
||||
|
||||
func newHandleStreamTest(t *testing.T, statsHandler stats.Handler) *handleStreamTest {
|
||||
bodyr, bodyw := io.Pipe()
|
||||
req := &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/grpc"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
Body: bodyr,
|
||||
}
|
||||
rw := newTestHandlerResponseWriter().(testHandlerResponseWriter)
|
||||
ht, err := NewServerHandlerTransport(rw, req, statsHandler, mem.DefaultBufferPool())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &handleStreamTest{
|
||||
t: t,
|
||||
bodyw: bodyw,
|
||||
ht: ht.(*serverHandlerTransport),
|
||||
rw: rw,
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestHandlerTransport_HandleStreams(t *testing.T) {
|
||||
st := newHandleStreamTest(t, nil)
|
||||
handleStream := func(s *ServerStream) {
|
||||
if want := "/service/foo.bar"; s.method != want {
|
||||
t.Errorf("stream method = %q; want %q", s.method, want)
|
||||
}
|
||||
|
||||
if err := s.SetHeader(metadata.Pairs("custom-header", "Custom header value")); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := s.SetTrailer(metadata.Pairs("custom-trailer", "Custom trailer value")); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := s.SetSendCompress("gzip"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
md := metadata.Pairs("custom-header", "Another custom header value")
|
||||
if err := s.SendHeader(md); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
delete(md, "custom-header")
|
||||
|
||||
if err := s.SetHeader(metadata.Pairs("too-late", "Header value that should be ignored")); err == nil {
|
||||
t.Error("expected SetHeader call after SendHeader to fail")
|
||||
}
|
||||
|
||||
if err := s.SendHeader(metadata.Pairs("too-late", "This header value should be ignored as well")); err == nil {
|
||||
t.Error("expected second SendHeader call to fail")
|
||||
}
|
||||
|
||||
if err := s.SetSendCompress("snappy"); err == nil {
|
||||
t.Error("expected second SetSendCompress call to fail")
|
||||
}
|
||||
|
||||
st.bodyw.Close() // no body
|
||||
s.WriteStatus(status.New(codes.OK, ""))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
st.ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go handleStream(s) },
|
||||
)
|
||||
wantHeader := http.Header{
|
||||
"Date": nil,
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
"Custom-Header": {"Custom header value", "Another custom header value"},
|
||||
"Grpc-Encoding": {"gzip"},
|
||||
}
|
||||
wantTrailer := http.Header{
|
||||
"Grpc-Status": {"0"},
|
||||
"Custom-Trailer": {"Custom trailer value"},
|
||||
}
|
||||
checkHeaderAndTrailer(t, st.rw, wantHeader, wantTrailer)
|
||||
}
|
||||
|
||||
// Tests that codes.Unimplemented will close the body, per comment in handler_server.go.
|
||||
func (s) TestHandlerTransport_HandleStreams_Unimplemented(t *testing.T) {
|
||||
handleStreamCloseBodyTest(t, codes.Unimplemented, "thingy is unimplemented")
|
||||
}
|
||||
|
||||
// Tests that codes.InvalidArgument will close the body, per comment in handler_server.go.
|
||||
func (s) TestHandlerTransport_HandleStreams_InvalidArgument(t *testing.T) {
|
||||
handleStreamCloseBodyTest(t, codes.InvalidArgument, "bad arg")
|
||||
}
|
||||
|
||||
func handleStreamCloseBodyTest(t *testing.T, statusCode codes.Code, msg string) {
|
||||
st := newHandleStreamTest(t, nil)
|
||||
|
||||
handleStream := func(s *ServerStream) {
|
||||
s.WriteStatus(status.New(statusCode, msg))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
st.ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go handleStream(s) },
|
||||
)
|
||||
wantHeader := http.Header{
|
||||
"Date": nil,
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
}
|
||||
wantTrailer := http.Header{
|
||||
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
|
||||
"Grpc-Message": {encodeGrpcMessage(msg)},
|
||||
}
|
||||
checkHeaderAndTrailer(t, st.rw, wantHeader, wantTrailer)
|
||||
}
|
||||
|
||||
func (s) TestHandlerTransport_HandleStreams_Timeout(t *testing.T) {
|
||||
bodyr, bodyw := io.Pipe()
|
||||
req := &http.Request{
|
||||
ProtoMajor: 2,
|
||||
Method: "POST",
|
||||
Header: http.Header{
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Grpc-Timeout": {"200m"},
|
||||
},
|
||||
URL: &url.URL{
|
||||
Path: "/service/foo.bar",
|
||||
},
|
||||
Body: bodyr,
|
||||
}
|
||||
rw := newTestHandlerResponseWriter().(testHandlerResponseWriter)
|
||||
ht, err := NewServerHandlerTransport(rw, req, nil, mem.DefaultBufferPool())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runStream := func(s *ServerStream) {
|
||||
defer bodyw.Close()
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("timeout waiting for ctx.Done")
|
||||
return
|
||||
}
|
||||
err := s.ctx.Err()
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Errorf("ctx.Err = %v; want %v", err, context.DeadlineExceeded)
|
||||
return
|
||||
}
|
||||
s.WriteStatus(status.New(codes.DeadlineExceeded, "too slow"))
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go runStream(s) },
|
||||
)
|
||||
wantHeader := http.Header{
|
||||
"Date": nil,
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
}
|
||||
wantTrailer := http.Header{
|
||||
"Grpc-Status": {"4"},
|
||||
"Grpc-Message": {encodeGrpcMessage("too slow")},
|
||||
}
|
||||
checkHeaderAndTrailer(t, rw, wantHeader, wantTrailer)
|
||||
}
|
||||
|
||||
// TestHandlerTransport_HandleStreams_MultiWriteStatus ensures that
|
||||
// concurrent "WriteStatus"s do not panic writing to closed "writes" channel.
|
||||
func (s) TestHandlerTransport_HandleStreams_MultiWriteStatus(t *testing.T) {
|
||||
testHandlerTransportHandleStreams(t, func(st *handleStreamTest, s *ServerStream) {
|
||||
if want := "/service/foo.bar"; s.method != want {
|
||||
t.Errorf("stream method = %q; want %q", s.method, want)
|
||||
}
|
||||
st.bodyw.Close() // no body
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.WriteStatus(status.New(codes.OK, ""))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
// TestHandlerTransport_HandleStreams_WriteStatusWrite ensures that "Write"
|
||||
// following "WriteStatus" does not panic writing to closed "writes" channel.
|
||||
func (s) TestHandlerTransport_HandleStreams_WriteStatusWrite(t *testing.T) {
|
||||
testHandlerTransportHandleStreams(t, func(st *handleStreamTest, s *ServerStream) {
|
||||
if want := "/service/foo.bar"; s.method != want {
|
||||
t.Errorf("stream method = %q; want %q", s.method, want)
|
||||
}
|
||||
st.bodyw.Close() // no body
|
||||
|
||||
s.WriteStatus(status.New(codes.OK, ""))
|
||||
s.Write([]byte("hdr"), newBufferSlice([]byte("data")), &WriteOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func testHandlerTransportHandleStreams(t *testing.T, handleStream func(st *handleStreamTest, s *ServerStream)) {
|
||||
st := newHandleStreamTest(t, nil)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
t.Cleanup(cancel)
|
||||
st.ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go handleStream(st, s) },
|
||||
)
|
||||
}
|
||||
|
||||
func (s) TestHandlerTransport_HandleStreams_ErrDetails(t *testing.T) {
|
||||
errDetails := []protoadapt.MessageV1{
|
||||
&epb.RetryInfo{
|
||||
RetryDelay: &durationpb.Duration{Seconds: 60},
|
||||
},
|
||||
&epb.ResourceInfo{
|
||||
ResourceType: "foo bar",
|
||||
ResourceName: "service.foo.bar",
|
||||
Owner: "User",
|
||||
},
|
||||
}
|
||||
|
||||
statusCode := codes.ResourceExhausted
|
||||
msg := "you are being throttled"
|
||||
st, err := status.New(statusCode, msg).WithDetails(errDetails...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stBytes, err := proto.Marshal(st.Proto())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hst := newHandleStreamTest(t, nil)
|
||||
handleStream := func(s *ServerStream) {
|
||||
s.WriteStatus(st)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
hst.ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go handleStream(s) },
|
||||
)
|
||||
wantHeader := http.Header{
|
||||
"Date": nil,
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
}
|
||||
wantTrailer := http.Header{
|
||||
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
|
||||
"Grpc-Message": {encodeGrpcMessage(msg)},
|
||||
"Grpc-Status-Details-Bin": {encodeBinHeader(stBytes)},
|
||||
}
|
||||
|
||||
checkHeaderAndTrailer(t, hst.rw, wantHeader, wantTrailer)
|
||||
}
|
||||
|
||||
// Tests the use of stats handlers and ensures there are no data races while
|
||||
// accessing trailers.
|
||||
func (s) TestHandlerTransport_HandleStreams_StatsHandlers(t *testing.T) {
|
||||
errDetails := []protoadapt.MessageV1{
|
||||
&epb.RetryInfo{
|
||||
RetryDelay: &durationpb.Duration{Seconds: 60},
|
||||
},
|
||||
&epb.ResourceInfo{
|
||||
ResourceType: "foo bar",
|
||||
ResourceName: "service.foo.bar",
|
||||
Owner: "User",
|
||||
},
|
||||
}
|
||||
|
||||
statusCode := codes.ResourceExhausted
|
||||
msg := "you are being throttled"
|
||||
st, err := status.New(statusCode, msg).WithDetails(errDetails...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stBytes, err := proto.Marshal(st.Proto())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add mock stats handlers to exercise the stats handler code path.
|
||||
statsHandler := &mockStatsHandler{
|
||||
rpcStatsCh: make(chan stats.RPCStats, 2),
|
||||
}
|
||||
hst := newHandleStreamTest(t, statsHandler)
|
||||
handleStream := func(s *ServerStream) {
|
||||
if err := s.SendHeader(metadata.New(map[string]string{})); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := s.SetTrailer(metadata.Pairs("custom-trailer", "Custom trailer value")); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
s.WriteStatus(st)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
hst.ht.HandleStreams(
|
||||
ctx, func(s *ServerStream) { go handleStream(s) },
|
||||
)
|
||||
wantHeader := http.Header{
|
||||
"Date": nil,
|
||||
"Content-Type": {"application/grpc"},
|
||||
"Trailer": {"Grpc-Status", "Grpc-Message", "Grpc-Status-Details-Bin"},
|
||||
}
|
||||
wantTrailer := http.Header{
|
||||
"Grpc-Status": {fmt.Sprint(uint32(statusCode))},
|
||||
"Grpc-Message": {encodeGrpcMessage(msg)},
|
||||
"Grpc-Status-Details-Bin": {encodeBinHeader(stBytes)},
|
||||
"Custom-Trailer": []string{"Custom trailer value"},
|
||||
}
|
||||
|
||||
checkHeaderAndTrailer(t, hst.rw, wantHeader, wantTrailer)
|
||||
wantStatTypes := []stats.RPCStats{&stats.OutHeader{}, &stats.OutTrailer{}}
|
||||
for _, wantType := range wantStatTypes {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Context timed out waiting for statsHandler.HandleRPC() to be called.")
|
||||
case s := <-statsHandler.rpcStatsCh:
|
||||
if reflect.TypeOf(s) != reflect.TypeOf(wantType) {
|
||||
t.Fatalf("Received RPCStats of type %T, want %T", s, wantType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHeaderAndTrailer checks that the resulting header and trailer matches the expectation.
|
||||
func checkHeaderAndTrailer(t *testing.T, rw testHandlerResponseWriter, wantHeader, wantTrailer http.Header) {
|
||||
// For trailer-only responses, the trailer values might be reported as part of the Header. They will however
|
||||
// be present in Trailer in either case. Hence, normalize the header by removing all trailer values.
|
||||
actualHeader := rw.Result().Header.Clone()
|
||||
for _, trailerKey := range actualHeader["Trailer"] {
|
||||
actualHeader.Del(trailerKey)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actualHeader, wantHeader) {
|
||||
t.Errorf("Header mismatch.\n got: %#v\n want: %#v", actualHeader, wantHeader)
|
||||
}
|
||||
if actualTrailer := rw.Result().Trailer; !reflect.DeepEqual(actualTrailer, wantTrailer) {
|
||||
t.Errorf("Trailer mismatch.\n got: %#v\n want: %#v", actualTrailer, wantTrailer)
|
||||
}
|
||||
}
|
||||
1921
internal/transport/http2_client.go
Normal file
1921
internal/transport/http2_client.go
Normal file
File diff suppressed because it is too large
Load Diff
96
internal/transport/http2_client_header_bench_test.go
Normal file
96
internal/transport/http2_client_header_bench_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
*
|
||||
* 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// BenchmarkCreateHeaderFields measures header-slice construction as the amount
|
||||
// of outgoing metadata varies.
|
||||
func BenchmarkCreateHeaderFields(b *testing.B) {
|
||||
for _, mdCount := range []int{0, 4, 12} {
|
||||
b.Run(fmt.Sprintf("mdCount=%d", mdCount), func(b *testing.B) {
|
||||
t := &http2Client{
|
||||
scheme: "https",
|
||||
userAgent: "grpc-go/benchmark",
|
||||
md: metadata.MD{},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
if mdCount > 0 {
|
||||
md := make(metadata.MD, mdCount)
|
||||
for i := 0; i < mdCount; i++ {
|
||||
md[fmt.Sprintf("header-%d", i)] = []string{fmt.Sprintf("value-%d", i)}
|
||||
}
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
}
|
||||
callHdr := &CallHdr{
|
||||
Method: "/grpc.testing.BenchmarkService/UnaryCall",
|
||||
Host: "server:443",
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
hf, err := t.createHeaderFields(ctx, callHdr)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if len(hf) == 0 {
|
||||
b.Fatal("no header fields produced")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(fmt.Sprintf("mdCount=%d-appended", mdCount), func(b *testing.B) {
|
||||
t := &http2Client{
|
||||
scheme: "https",
|
||||
userAgent: "grpc-go/benchmark",
|
||||
md: metadata.MD{},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
kvs := make([]string, 0, mdCount*2)
|
||||
for i := 0; i < mdCount; i++ {
|
||||
kvs = append(kvs, fmt.Sprintf("header-%d", i), fmt.Sprintf("value-%d", i))
|
||||
}
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, kvs...)
|
||||
callHdr := &CallHdr{
|
||||
Method: "/grpc.testing.BenchmarkService/UnaryCall",
|
||||
Host: "server:443",
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
hf, err := t.createHeaderFields(ctx, callHdr)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if len(hf) == 0 {
|
||||
b.Fatal("no header fields produced")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1532
internal/transport/http2_server.go
Normal file
1532
internal/transport/http2_server.go
Normal file
File diff suppressed because it is too large
Load Diff
643
internal/transport/http_util.go
Normal file
643
internal/transport/http_util.go
Normal file
@@ -0,0 +1,643 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2014 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 (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
imem "google.golang.org/grpc/internal/mem"
|
||||
"google.golang.org/grpc/internal/transport/readyreader"
|
||||
"google.golang.org/grpc/mem"
|
||||
)
|
||||
|
||||
const (
|
||||
// http2MaxFrameLen specifies the max length of a HTTP2 frame.
|
||||
http2MaxFrameLen = 16384 // 16KB frame
|
||||
// https://httpwg.org/specs/rfc7540.html#SettingValues
|
||||
http2InitHeaderTableSize = 4096
|
||||
)
|
||||
|
||||
var (
|
||||
clientPreface = []byte(http2.ClientPreface)
|
||||
http2ErrConvTab = map[http2.ErrCode]codes.Code{
|
||||
http2.ErrCodeNo: codes.Internal,
|
||||
http2.ErrCodeProtocol: codes.Internal,
|
||||
http2.ErrCodeInternal: codes.Internal,
|
||||
http2.ErrCodeFlowControl: codes.ResourceExhausted,
|
||||
http2.ErrCodeSettingsTimeout: codes.Internal,
|
||||
http2.ErrCodeStreamClosed: codes.Internal,
|
||||
http2.ErrCodeFrameSize: codes.Internal,
|
||||
http2.ErrCodeRefusedStream: codes.Unavailable,
|
||||
http2.ErrCodeCancel: codes.Canceled,
|
||||
http2.ErrCodeCompression: codes.Internal,
|
||||
http2.ErrCodeConnect: codes.Internal,
|
||||
http2.ErrCodeEnhanceYourCalm: codes.ResourceExhausted,
|
||||
http2.ErrCodeInadequateSecurity: codes.PermissionDenied,
|
||||
http2.ErrCodeHTTP11Required: codes.Internal,
|
||||
}
|
||||
// HTTPStatusConvTab is the HTTP status code to gRPC error code conversion table.
|
||||
HTTPStatusConvTab = map[int]codes.Code{
|
||||
// 400 Bad Request - INTERNAL.
|
||||
http.StatusBadRequest: codes.Internal,
|
||||
// 401 Unauthorized - UNAUTHENTICATED.
|
||||
http.StatusUnauthorized: codes.Unauthenticated,
|
||||
// 403 Forbidden - PERMISSION_DENIED.
|
||||
http.StatusForbidden: codes.PermissionDenied,
|
||||
// 404 Not Found - UNIMPLEMENTED.
|
||||
http.StatusNotFound: codes.Unimplemented,
|
||||
// 429 Too Many Requests - UNAVAILABLE.
|
||||
http.StatusTooManyRequests: codes.Unavailable,
|
||||
// 502 Bad Gateway - UNAVAILABLE.
|
||||
http.StatusBadGateway: codes.Unavailable,
|
||||
// 503 Service Unavailable - UNAVAILABLE.
|
||||
http.StatusServiceUnavailable: codes.Unavailable,
|
||||
// 504 Gateway timeout - UNAVAILABLE.
|
||||
http.StatusGatewayTimeout: codes.Unavailable,
|
||||
}
|
||||
)
|
||||
|
||||
var grpcStatusDetailsBinHeader = "grpc-status-details-bin"
|
||||
|
||||
// isReservedHeader checks whether hdr belongs to HTTP2 headers
|
||||
// reserved by gRPC protocol. Any other headers are classified as the
|
||||
// user-specified metadata.
|
||||
func isReservedHeader(hdr string) bool {
|
||||
if hdr != "" && hdr[0] == ':' {
|
||||
return true
|
||||
}
|
||||
switch hdr {
|
||||
case "content-type",
|
||||
"user-agent",
|
||||
"grpc-message-type",
|
||||
"grpc-encoding",
|
||||
"grpc-message",
|
||||
"grpc-status",
|
||||
"grpc-timeout",
|
||||
// Intentionally exclude grpc-previous-rpc-attempts and
|
||||
// grpc-retry-pushback-ms, which are "reserved", but their API
|
||||
// intentionally works via metadata.
|
||||
"te":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isWhitelistedHeader checks whether hdr should be propagated into metadata
|
||||
// visible to users, even though it is classified as "reserved", above.
|
||||
func isWhitelistedHeader(hdr string) bool {
|
||||
switch hdr {
|
||||
case ":authority", "user-agent":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const binHdrSuffix = "-bin"
|
||||
|
||||
func encodeBinHeader(v []byte) string {
|
||||
return base64.RawStdEncoding.EncodeToString(v)
|
||||
}
|
||||
|
||||
func decodeBinHeader(v string) ([]byte, error) {
|
||||
if len(v)%4 == 0 {
|
||||
// Input was padded, or padding was not necessary.
|
||||
return base64.StdEncoding.DecodeString(v)
|
||||
}
|
||||
return base64.RawStdEncoding.DecodeString(v)
|
||||
}
|
||||
|
||||
func encodeMetadataHeader(k, v string) string {
|
||||
if strings.HasSuffix(k, binHdrSuffix) {
|
||||
return encodeBinHeader(([]byte)(v))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func decodeMetadataHeader(k, v string) (string, error) {
|
||||
if strings.HasSuffix(k, binHdrSuffix) {
|
||||
b, err := decodeBinHeader(v)
|
||||
return string(b), err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type timeoutUnit uint8
|
||||
|
||||
const (
|
||||
hour timeoutUnit = 'H'
|
||||
minute timeoutUnit = 'M'
|
||||
second timeoutUnit = 'S'
|
||||
millisecond timeoutUnit = 'm'
|
||||
microsecond timeoutUnit = 'u'
|
||||
nanosecond timeoutUnit = 'n'
|
||||
)
|
||||
|
||||
func timeoutUnitToDuration(u timeoutUnit) (d time.Duration, ok bool) {
|
||||
switch u {
|
||||
case hour:
|
||||
return time.Hour, true
|
||||
case minute:
|
||||
return time.Minute, true
|
||||
case second:
|
||||
return time.Second, true
|
||||
case millisecond:
|
||||
return time.Millisecond, true
|
||||
case microsecond:
|
||||
return time.Microsecond, true
|
||||
case nanosecond:
|
||||
return time.Nanosecond, true
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decodeTimeout(s string) (time.Duration, error) {
|
||||
size := len(s)
|
||||
if size < 2 {
|
||||
return 0, fmt.Errorf("transport: timeout string is too short: %q", s)
|
||||
}
|
||||
if size > 9 {
|
||||
// Spec allows for 8 digits plus the unit.
|
||||
return 0, fmt.Errorf("transport: timeout string is too long: %q", s)
|
||||
}
|
||||
unit := timeoutUnit(s[size-1])
|
||||
d, ok := timeoutUnitToDuration(unit)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("transport: timeout unit is not recognized: %q", s)
|
||||
}
|
||||
t, err := strconv.ParseUint(s[:size-1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
const maxHours = math.MaxInt64 / uint64(time.Hour)
|
||||
if d == time.Hour && t > maxHours {
|
||||
// This timeout would overflow math.MaxInt64; clamp it.
|
||||
return time.Duration(math.MaxInt64), nil
|
||||
}
|
||||
return d * time.Duration(t), nil
|
||||
}
|
||||
|
||||
const (
|
||||
spaceByte = ' '
|
||||
tildeByte = '~'
|
||||
percentByte = '%'
|
||||
)
|
||||
|
||||
// encodeGrpcMessage is used to encode status code in header field
|
||||
// "grpc-message". It does percent encoding and also replaces invalid utf-8
|
||||
// characters with Unicode replacement character.
|
||||
//
|
||||
// It checks to see if each individual byte in msg is an allowable byte, and
|
||||
// then either percent encoding or passing it through. When percent encoding,
|
||||
// the byte is converted into hexadecimal notation with a '%' prepended.
|
||||
func encodeGrpcMessage(msg string) string {
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
lenMsg := len(msg)
|
||||
for i := 0; i < lenMsg; i++ {
|
||||
c := msg[i]
|
||||
if !(c >= spaceByte && c <= tildeByte && c != percentByte) {
|
||||
return encodeGrpcMessageUnchecked(msg)
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func encodeGrpcMessageUnchecked(msg string) string {
|
||||
var sb strings.Builder
|
||||
for len(msg) > 0 {
|
||||
r, size := utf8.DecodeRuneInString(msg)
|
||||
for _, b := range []byte(string(r)) {
|
||||
if size > 1 {
|
||||
// If size > 1, r is not ascii. Always do percent encoding.
|
||||
fmt.Fprintf(&sb, "%%%02X", b)
|
||||
continue
|
||||
}
|
||||
|
||||
// The for loop is necessary even if size == 1. r could be
|
||||
// utf8.RuneError.
|
||||
//
|
||||
// fmt.Sprintf("%%%02X", utf8.RuneError) gives "%FFFD".
|
||||
if b >= spaceByte && b <= tildeByte && b != percentByte {
|
||||
sb.WriteByte(b)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "%%%02X", b)
|
||||
}
|
||||
}
|
||||
msg = msg[size:]
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// decodeGrpcMessage decodes the msg encoded by encodeGrpcMessage.
|
||||
func decodeGrpcMessage(msg string) string {
|
||||
if msg == "" {
|
||||
return ""
|
||||
}
|
||||
lenMsg := len(msg)
|
||||
for i := 0; i < lenMsg; i++ {
|
||||
if msg[i] == percentByte && i+2 < lenMsg {
|
||||
return decodeGrpcMessageUnchecked(msg)
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func decodeGrpcMessageUnchecked(msg string) string {
|
||||
var sb strings.Builder
|
||||
lenMsg := len(msg)
|
||||
for i := 0; i < lenMsg; i++ {
|
||||
c := msg[i]
|
||||
if c == percentByte && i+2 < lenMsg {
|
||||
parsed, err := strconv.ParseUint(msg[i+1:i+3], 16, 8)
|
||||
if err != nil {
|
||||
sb.WriteByte(c)
|
||||
} else {
|
||||
sb.WriteByte(byte(parsed))
|
||||
i += 2
|
||||
}
|
||||
} else {
|
||||
sb.WriteByte(c)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type bufWriter struct {
|
||||
pool *imem.SimpleBufferPool
|
||||
bufHandle *[]byte
|
||||
offset int
|
||||
batchSize int
|
||||
conn io.Writer
|
||||
err error
|
||||
}
|
||||
|
||||
// unsharedBufWriter keeps the unshared slice header in the writer allocation.
|
||||
type unsharedBufWriter struct {
|
||||
bufWriter
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newBufWriter(conn io.Writer, batchSize int, pool *imem.SimpleBufferPool) *bufWriter {
|
||||
if pool == nil && batchSize > 0 {
|
||||
w := &unsharedBufWriter{
|
||||
bufWriter: bufWriter{batchSize: batchSize, conn: conn},
|
||||
buf: make([]byte, batchSize),
|
||||
}
|
||||
w.bufHandle = &w.buf
|
||||
return &w.bufWriter
|
||||
}
|
||||
return &bufWriter{batchSize: batchSize, conn: conn, pool: pool}
|
||||
}
|
||||
|
||||
func (w *bufWriter) Write(b []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
if w.batchSize == 0 { // Buffer has been disabled.
|
||||
n, err := w.conn.Write(b)
|
||||
return n, toIOError(err)
|
||||
}
|
||||
if w.bufHandle == nil {
|
||||
w.bufHandle = w.pool.Get(w.batchSize)
|
||||
}
|
||||
buf := *w.bufHandle
|
||||
written := 0
|
||||
for len(b) > 0 {
|
||||
copied := copy(buf[w.offset:], b)
|
||||
b = b[copied:]
|
||||
written += copied
|
||||
w.offset += copied
|
||||
if w.offset < w.batchSize {
|
||||
continue
|
||||
}
|
||||
if err := w.flushKeepBuffer(); err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (w *bufWriter) Flush() error {
|
||||
err := w.flushKeepBuffer()
|
||||
w.releaseBuffer()
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *bufWriter) releaseBuffer() {
|
||||
if w.pool != nil && w.bufHandle != nil {
|
||||
w.pool.Put(w.bufHandle)
|
||||
w.bufHandle = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *bufWriter) flushKeepBuffer() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
if w.offset == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := *w.bufHandle
|
||||
_, w.err = w.conn.Write(buf[:w.offset])
|
||||
w.err = toIOError(w.err)
|
||||
w.offset = 0
|
||||
if w.err != nil {
|
||||
w.releaseBuffer()
|
||||
}
|
||||
return w.err
|
||||
}
|
||||
|
||||
type ioError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (i ioError) Unwrap() error {
|
||||
return i.error
|
||||
}
|
||||
|
||||
func isIOError(err error) bool {
|
||||
return errors.As(err, &ioError{})
|
||||
}
|
||||
|
||||
func toIOError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return ioError{error: err}
|
||||
}
|
||||
|
||||
type parsedDataFrame struct {
|
||||
http2.FrameHeader
|
||||
data mem.Buffer
|
||||
}
|
||||
|
||||
func (df *parsedDataFrame) StreamEnded() bool {
|
||||
return df.FrameHeader.Flags.Has(http2.FlagDataEndStream)
|
||||
}
|
||||
|
||||
type framer struct {
|
||||
writer *bufWriter
|
||||
fr *http2.Framer
|
||||
headerBuf []byte // cached slice for framer headers to reduce heap allocs.
|
||||
reader io.Reader
|
||||
dataFrame parsedDataFrame // Cached data frame to avoid heap allocations.
|
||||
pool mem.BufferPool
|
||||
errDetail error
|
||||
}
|
||||
|
||||
var ioBufferPoolMap = make(map[int]*imem.SimpleBufferPool)
|
||||
var ioBufferMutex sync.Mutex
|
||||
|
||||
func bufferedReader(r io.Reader, bufSize int) io.Reader {
|
||||
if bufSize <= 0 {
|
||||
return r
|
||||
}
|
||||
if envconfig.EnableHTTPFramerReadBufferPooling {
|
||||
if rr := readyreader.NewNonBlocking(r); rr != nil {
|
||||
readPool := ioBufferPool(bufSize)
|
||||
return readyreader.NewBuffered(rr, bufSize, readPool)
|
||||
}
|
||||
}
|
||||
return bufio.NewReaderSize(r, bufSize)
|
||||
}
|
||||
|
||||
func newFramer(conn io.ReadWriter, writeBufferSize, readBufferSize int, sharedWriteBuffer bool, maxHeaderListSize uint32, memPool mem.BufferPool) *framer {
|
||||
if writeBufferSize < 0 {
|
||||
writeBufferSize = 0
|
||||
}
|
||||
r := bufferedReader(conn, readBufferSize)
|
||||
var writePool *imem.SimpleBufferPool
|
||||
if sharedWriteBuffer {
|
||||
writePool = ioBufferPool(writeBufferSize)
|
||||
}
|
||||
w := newBufWriter(conn, writeBufferSize, writePool)
|
||||
f := &framer{
|
||||
writer: w,
|
||||
fr: http2.NewFramer(w, r),
|
||||
reader: r,
|
||||
pool: memPool,
|
||||
}
|
||||
f.fr.SetMaxReadFrameSize(http2MaxFrameLen)
|
||||
// Opt-in to Frame reuse API on framer to reduce garbage.
|
||||
// Frames aren't safe to read from after a subsequent call to ReadFrame.
|
||||
f.fr.SetReuseFrames()
|
||||
f.fr.MaxHeaderListSize = maxHeaderListSize
|
||||
f.fr.ReadMetaHeaders = hpack.NewDecoder(http2InitHeaderTableSize, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
// writeData writes a DATA frame.
|
||||
//
|
||||
// It is the caller's responsibility not to violate the maximum frame size.
|
||||
func (f *framer) writeData(streamID uint32, endStream bool, data [][]byte) error {
|
||||
var flags http2.Flags
|
||||
if endStream {
|
||||
flags = http2.FlagDataEndStream
|
||||
}
|
||||
length := uint32(0)
|
||||
for _, d := range data {
|
||||
length += uint32(len(d))
|
||||
}
|
||||
// TODO: Replace the header write with the framer API being added in
|
||||
// https://github.com/golang/go/issues/66655.
|
||||
f.headerBuf = append(f.headerBuf[:0],
|
||||
byte(length>>16),
|
||||
byte(length>>8),
|
||||
byte(length),
|
||||
byte(http2.FrameData),
|
||||
byte(flags),
|
||||
byte(streamID>>24),
|
||||
byte(streamID>>16),
|
||||
byte(streamID>>8),
|
||||
byte(streamID))
|
||||
if _, err := f.writer.Write(f.headerBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range data {
|
||||
if _, err := f.writer.Write(d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readFrame reads a single frame. The returned Frame is only valid
|
||||
// until the next call to readFrame.
|
||||
func (f *framer) readFrame() (any, error) {
|
||||
f.errDetail = nil
|
||||
fh, err := f.fr.ReadFrameHeader()
|
||||
if err != nil {
|
||||
f.errDetail = f.fr.ErrorDetail()
|
||||
return nil, err
|
||||
}
|
||||
// Read the data frame directly from the underlying io.Reader to avoid
|
||||
// copies.
|
||||
if fh.Type == http2.FrameData {
|
||||
err = f.readDataFrame(fh)
|
||||
return &f.dataFrame, err
|
||||
}
|
||||
fr, err := f.fr.ReadFrameForHeader(fh)
|
||||
if err != nil {
|
||||
f.errDetail = f.fr.ErrorDetail()
|
||||
return nil, err
|
||||
}
|
||||
return fr, err
|
||||
}
|
||||
|
||||
// errorDetail returns a more detailed error of the last error
|
||||
// returned by framer.readFrame. For instance, if readFrame
|
||||
// returns a StreamError with code PROTOCOL_ERROR, errorDetail
|
||||
// will say exactly what was invalid. errorDetail is not guaranteed
|
||||
// to return a non-nil value.
|
||||
// errorDetail is reset after the next call to readFrame.
|
||||
func (f *framer) errorDetail() error {
|
||||
return f.errDetail
|
||||
}
|
||||
|
||||
func (f *framer) readDataFrame(fh http2.FrameHeader) (err error) {
|
||||
if fh.StreamID == 0 {
|
||||
// DATA frames MUST be associated with a stream. If a
|
||||
// DATA frame is received whose stream identifier
|
||||
// field is 0x0, the recipient MUST respond with a
|
||||
// connection error (Section 5.4.1) of type
|
||||
// PROTOCOL_ERROR.
|
||||
f.errDetail = errors.New("DATA frame with stream ID 0")
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
// Converting a *[]byte to a mem.SliceBuffer incurs a heap allocation. This
|
||||
// conversion is performed by mem.NewBuffer. To avoid the extra allocation
|
||||
// a []byte is allocated directly if required and cast to a mem.SliceBuffer.
|
||||
var buf []byte
|
||||
// poolHandle is the pointer returned by the buffer pool (if it's used.).
|
||||
var poolHandle *[]byte
|
||||
useBufferPool := !mem.IsBelowBufferPoolingThreshold(int(fh.Length))
|
||||
if useBufferPool {
|
||||
poolHandle = f.pool.Get(int(fh.Length))
|
||||
buf = *poolHandle
|
||||
defer func() {
|
||||
if err != nil {
|
||||
f.pool.Put(poolHandle)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
buf = make([]byte, int(fh.Length))
|
||||
}
|
||||
if fh.Flags.Has(http2.FlagDataPadded) {
|
||||
if fh.Length == 0 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
// This initial 1-byte read can be inefficient for unbuffered readers,
|
||||
// but it allows the rest of the payload to be read directly to the
|
||||
// start of the destination slice. This makes it easy to return the
|
||||
// original slice back to the buffer pool.
|
||||
if _, err := io.ReadFull(f.reader, buf[:1]); err != nil {
|
||||
return err
|
||||
}
|
||||
padSize := buf[0]
|
||||
buf = buf[:len(buf)-1]
|
||||
if int(padSize) > len(buf) {
|
||||
// If the length of the padding is greater than the
|
||||
// length of the frame payload, the recipient MUST
|
||||
// treat this as a connection error.
|
||||
// Filed: https://github.com/http2/http2-spec/issues/610
|
||||
f.errDetail = errors.New("pad size larger than data payload")
|
||||
return http2.ConnectionError(http2.ErrCodeProtocol)
|
||||
}
|
||||
if _, err := io.ReadFull(f.reader, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:len(buf)-int(padSize)]
|
||||
} else if _, err := io.ReadFull(f.reader, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.dataFrame.FrameHeader = fh
|
||||
if useBufferPool {
|
||||
// Update the handle to point to the (potentially re-sliced) buf.
|
||||
*poolHandle = buf
|
||||
f.dataFrame.data = mem.NewBuffer(poolHandle, f.pool)
|
||||
} else {
|
||||
f.dataFrame.data = mem.SliceBuffer(buf)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (df *parsedDataFrame) Header() http2.FrameHeader {
|
||||
return df.FrameHeader
|
||||
}
|
||||
|
||||
func ioBufferPool(size int) *imem.SimpleBufferPool {
|
||||
ioBufferMutex.Lock()
|
||||
defer ioBufferMutex.Unlock()
|
||||
pool, ok := ioBufferPoolMap[size]
|
||||
if ok {
|
||||
return pool
|
||||
}
|
||||
pool = imem.NewDirtySimplePool()
|
||||
ioBufferPoolMap[size] = pool
|
||||
return pool
|
||||
}
|
||||
|
||||
// ParseDialTarget returns the network and address to pass to dialer.
|
||||
func ParseDialTarget(target string) (string, string) {
|
||||
net := "tcp"
|
||||
m1 := strings.Index(target, ":")
|
||||
m2 := strings.Index(target, ":/")
|
||||
// handle unix:addr which will fail with url.Parse
|
||||
if m1 >= 0 && m2 < 0 {
|
||||
if n := target[0:m1]; n == "unix" {
|
||||
return n, target[m1+1:]
|
||||
}
|
||||
}
|
||||
if m2 >= 0 {
|
||||
t, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return net, target
|
||||
}
|
||||
scheme := t.Scheme
|
||||
addr := t.Path
|
||||
if scheme == "unix" {
|
||||
if addr == "" {
|
||||
addr = t.Host
|
||||
}
|
||||
return scheme, addr
|
||||
}
|
||||
}
|
||||
return net, target
|
||||
}
|
||||
637
internal/transport/http_util_test.go
Normal file
637
internal/transport/http_util_test.go
Normal file
@@ -0,0 +1,637 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2014 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
imem "google.golang.org/grpc/internal/mem"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/transport/readyreader"
|
||||
"google.golang.org/grpc/mem"
|
||||
)
|
||||
|
||||
func (s) TestDecodeTimeout(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
// input
|
||||
s string
|
||||
// output
|
||||
d time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
|
||||
{"00000001n", time.Nanosecond, false},
|
||||
{"10u", time.Microsecond * 10, false},
|
||||
{"00000010m", time.Millisecond * 10, false},
|
||||
{"1234S", time.Second * 1234, false},
|
||||
{"00000001M", time.Minute, false},
|
||||
{"09999999S", time.Second * 9999999, false},
|
||||
{"99999999S", time.Second * 99999999, false},
|
||||
{"99999999M", time.Minute * 99999999, false},
|
||||
{"2562047H", time.Hour * 2562047, false},
|
||||
{"2562048H", time.Duration(math.MaxInt64), false},
|
||||
{"99999999H", time.Duration(math.MaxInt64), false},
|
||||
{"-1S", 0, true},
|
||||
{"1234x", 0, true},
|
||||
{"1234s", 0, true},
|
||||
{"1234", 0, true},
|
||||
{"1", 0, true},
|
||||
{"", 0, true},
|
||||
{"9a1S", 0, true},
|
||||
{"0S", 0, false}, // PROTOCOL-HTTP2.md requires positive integers, but we allow it to timeout instead
|
||||
{"00000000S", 0, false},
|
||||
{"000000000S", 0, true}, // PROTOCOL-HTTP2.md allows at most 8 digits
|
||||
} {
|
||||
d, err := decodeTimeout(test.s)
|
||||
gotErr := err != nil
|
||||
if d != test.d || gotErr != test.wantErr {
|
||||
t.Errorf("timeoutDecode(%q) = %d, %v, want %d, wantErr=%v",
|
||||
test.s, int64(d), err, int64(test.d), test.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestEncodeGrpcMessage(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"Hello", "Hello"},
|
||||
{"\u0000", "%00"},
|
||||
{"%", "%25"},
|
||||
{"系统", "%E7%B3%BB%E7%BB%9F"},
|
||||
{string([]byte{0xff, 0xfe, 0xfd}), "%EF%BF%BD%EF%BF%BD%EF%BF%BD"},
|
||||
} {
|
||||
actual := encodeGrpcMessage(tt.input)
|
||||
if tt.expected != actual {
|
||||
t.Errorf("encodeGrpcMessage(%q) = %q, want %q", tt.input, actual, tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that all the visible ASCII chars except '%' are not percent encoded.
|
||||
for i := ' '; i <= '~' && i != '%'; i++ {
|
||||
output := encodeGrpcMessage(string(i))
|
||||
if output != string(i) {
|
||||
t.Errorf("encodeGrpcMessage(%v) = %v, want %v", string(i), output, string(i))
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that all the invisible ASCII chars and '%' are percent encoded.
|
||||
for i := rune(0); i == '%' || (i >= rune(0) && i < ' ') || (i > '~' && i <= rune(127)); i++ {
|
||||
output := encodeGrpcMessage(string(i))
|
||||
expected := fmt.Sprintf("%%%02X", i)
|
||||
if output != expected {
|
||||
t.Errorf("encodeGrpcMessage(%v) = %v, want %v", string(i), output, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestDecodeGrpcMessage(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"Hello", "Hello"},
|
||||
{"H%61o", "Hao"},
|
||||
{"H%6", "H%6"},
|
||||
{"%G0", "%G0"},
|
||||
{"%E7%B3%BB%E7%BB%9F", "系统"},
|
||||
{"%EF%BF%BD", "<22>"},
|
||||
} {
|
||||
actual := decodeGrpcMessage(tt.input)
|
||||
if tt.expected != actual {
|
||||
t.Errorf("decodeGrpcMessage(%q) = %q, want %q", tt.input, actual, tt.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that all the visible ASCII chars except '%' are not percent decoded.
|
||||
for i := ' '; i <= '~' && i != '%'; i++ {
|
||||
output := decodeGrpcMessage(string(i))
|
||||
if output != string(i) {
|
||||
t.Errorf("decodeGrpcMessage(%v) = %v, want %v", string(i), output, string(i))
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that all the invisible ASCII chars and '%' are percent decoded.
|
||||
for i := rune(0); i == '%' || (i >= rune(0) && i < ' ') || (i > '~' && i <= rune(127)); i++ {
|
||||
output := decodeGrpcMessage(fmt.Sprintf("%%%02X", i))
|
||||
if output != string(i) {
|
||||
t.Errorf("decodeGrpcMessage(%v) = %v, want %v", fmt.Sprintf("%%%02X", i), output, string(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decode an encoded string should get the same thing back, except for invalid
|
||||
// utf8 chars.
|
||||
func (s) TestDecodeEncodeGrpcMessage(t *testing.T) {
|
||||
testCases := []struct {
|
||||
orig string
|
||||
want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"hello", "hello"},
|
||||
{"h%6", "h%6"},
|
||||
{"%G0", "%G0"},
|
||||
{"系统", "系统"},
|
||||
{"Hello, 世界", "Hello, 世界"},
|
||||
|
||||
{string([]byte{0xff, 0xfe, 0xfd}), "<22><><EFBFBD>"},
|
||||
{string([]byte{0xff}) + "Hello" + string([]byte{0xfe}) + "世界" + string([]byte{0xfd}), "<22>Hello<6C>世界<E4B896>"},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
got := decodeGrpcMessage(encodeGrpcMessage(tC.orig))
|
||||
if got != tC.want {
|
||||
t.Errorf("decodeGrpcMessage(encodeGrpcMessage(%q)) = %q, want %q", tC.orig, got, tC.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const binaryValue = "\u0080"
|
||||
|
||||
func (s) TestEncodeMetadataHeader(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
// input
|
||||
kin string
|
||||
vin string
|
||||
// output
|
||||
vout string
|
||||
}{
|
||||
{"key", "abc", "abc"},
|
||||
{"KEY", "abc", "abc"},
|
||||
{"key-bin", "abc", "YWJj"},
|
||||
{"key-bin", binaryValue, "woA"},
|
||||
} {
|
||||
v := encodeMetadataHeader(test.kin, test.vin)
|
||||
if !reflect.DeepEqual(v, test.vout) {
|
||||
t.Fatalf("encodeMetadataHeader(%q, %q) = %q, want %q", test.kin, test.vin, v, test.vout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestDecodeMetadataHeader(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
// input
|
||||
kin string
|
||||
vin string
|
||||
// output
|
||||
vout string
|
||||
err error
|
||||
}{
|
||||
{"a", "abc", "abc", nil},
|
||||
{"key-bin", "Zm9vAGJhcg==", "foo\x00bar", nil},
|
||||
{"key-bin", "Zm9vAGJhcg", "foo\x00bar", nil},
|
||||
{"key-bin", "woA=", binaryValue, nil},
|
||||
{"a", "abc,efg", "abc,efg", nil},
|
||||
} {
|
||||
v, err := decodeMetadataHeader(test.kin, test.vin)
|
||||
if !reflect.DeepEqual(v, test.vout) || !reflect.DeepEqual(err, test.err) {
|
||||
t.Fatalf("decodeMetadataHeader(%q, %q) = %q, %v, want %q, %v", test.kin, test.vin, v, err, test.vout, test.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestParseDialTarget(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
target, wantNet, wantAddr string
|
||||
}{
|
||||
{"unix:a", "unix", "a"},
|
||||
{"unix:a/b/c", "unix", "a/b/c"},
|
||||
{"unix:/a", "unix", "/a"},
|
||||
{"unix:/a/b/c", "unix", "/a/b/c"},
|
||||
{"unix://a", "unix", "a"},
|
||||
{"unix://a/b/c", "unix", "/b/c"},
|
||||
{"unix:///a", "unix", "/a"},
|
||||
{"unix:///a/b/c", "unix", "/a/b/c"},
|
||||
{"unix:etcd:0", "unix", "etcd:0"},
|
||||
{"unix:///tmp/unix-3", "unix", "/tmp/unix-3"},
|
||||
{"unix://domain", "unix", "domain"},
|
||||
{"unix://etcd:0", "unix", "etcd:0"},
|
||||
{"unix:///etcd:0", "unix", "/etcd:0"},
|
||||
{"passthrough://unix://domain", "tcp", "passthrough://unix://domain"},
|
||||
{"https://google.com:443", "tcp", "https://google.com:443"},
|
||||
{"dns:///google.com", "tcp", "dns:///google.com"},
|
||||
{"/unix/socket/address", "tcp", "/unix/socket/address"},
|
||||
} {
|
||||
gotNet, gotAddr := ParseDialTarget(test.target)
|
||||
if gotNet != test.wantNet || gotAddr != test.wantAddr {
|
||||
t.Errorf("ParseDialTarget(%q) = %s, %s want %s, %s", test.target, gotNet, gotAddr, test.wantNet, test.wantAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type badNetworkConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *badNetworkConn) Write([]byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// This test ensures Write() on a broken network connection does not lead to
|
||||
// an infinite loop. See https://github.com/grpc/grpc-go/issues/7389 for more details.
|
||||
func (s) TestWriteBadConnection(t *testing.T) {
|
||||
data := []byte("test_data")
|
||||
// Configure the bufWriter with a batchsize that results in data being flushed
|
||||
// to the underlying conn, midway through Write().
|
||||
writeBufferSize := (len(data) - 1) / 2
|
||||
writer := newBufWriter(&badNetworkConn{}, writeBufferSize, ioBufferPool(writeBufferSize))
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := writer.Write(data)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Write() did not return in time")
|
||||
case err := <-errCh:
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("Write() = %v, want error presence = %v", err, io.EOF)
|
||||
}
|
||||
}
|
||||
if writer.bufHandle != nil {
|
||||
t.Fatalf("failed Write() retained pooled buffer: handle=%p", writer.bufHandle)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestBufWriterSharedFlushAllocations(t *testing.T) {
|
||||
pool := imem.NewDirtySimplePool()
|
||||
writer := newBufWriter(io.Discard, defaultWriteBufSize, pool)
|
||||
payload := make([]byte, 128)
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
if _, err := writer.Write(payload); err != nil {
|
||||
t.Fatalf("Write() failed: %v", err)
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
t.Fatalf("Flush() failed: %v", err)
|
||||
}
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("shared Write()+Flush() allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestBufWriterSharedBufferOwnership(t *testing.T) {
|
||||
const batchSize = 8
|
||||
pool := imem.NewDirtySimplePool()
|
||||
var conn bytes.Buffer
|
||||
writer := newBufWriter(&conn, batchSize, pool)
|
||||
|
||||
if _, err := writer.Write([]byte("abc")); err != nil {
|
||||
t.Fatalf("Write() failed: %v", err)
|
||||
}
|
||||
if writer.bufHandle == nil {
|
||||
t.Fatal("Write() did not retain the pooled buffer handle")
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
t.Fatalf("Flush() failed: %v", err)
|
||||
}
|
||||
if writer.bufHandle != nil {
|
||||
t.Fatalf("Flush() retained pooled buffer: handle=%p", writer.bufHandle)
|
||||
}
|
||||
if got := conn.String(); got != "abc" {
|
||||
t.Fatalf("conn data = %q, want %q", got, "abc")
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
t.Fatalf("second Flush() failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := writer.Write([]byte("12345678")); err != nil {
|
||||
t.Fatalf("full-buffer Write() failed: %v", err)
|
||||
}
|
||||
if writer.bufHandle == nil {
|
||||
t.Fatal("full-buffer Write() released the handle during its internal flush")
|
||||
}
|
||||
if writer.offset != 0 {
|
||||
t.Fatalf("full-buffer Write() offset = %d, want 0", writer.offset)
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
t.Fatalf("Flush() after full-buffer Write() failed: %v", err)
|
||||
}
|
||||
if writer.bufHandle != nil {
|
||||
t.Fatalf("Flush() retained pooled buffer: handle=%p", writer.bufHandle)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestBufWriterFlushErrorReleasesSharedBuffer(t *testing.T) {
|
||||
pool := imem.NewDirtySimplePool()
|
||||
writer := newBufWriter(&badNetworkConn{}, 8, pool)
|
||||
|
||||
if _, err := writer.Write([]byte("abc")); err != nil {
|
||||
t.Fatalf("Write() failed before flush: %v", err)
|
||||
}
|
||||
if writer.bufHandle == nil {
|
||||
t.Fatal("Write() did not retain the pooled buffer handle")
|
||||
}
|
||||
if err := writer.Flush(); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("Flush() error = %v, want %v", err, io.EOF)
|
||||
}
|
||||
if writer.bufHandle != nil {
|
||||
t.Fatalf("failed Flush() retained pooled buffer: handle=%p", writer.bufHandle)
|
||||
}
|
||||
if _, err := writer.Write([]byte("def")); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("Write() after failed Flush() error = %v, want %v", err, io.EOF)
|
||||
}
|
||||
if writer.bufHandle != nil {
|
||||
t.Fatal("Write() after failed Flush() acquired another pooled buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBufWriter(b *testing.B) {
|
||||
const payloadSize = 128
|
||||
payload := make([]byte, payloadSize)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
pool *imem.SimpleBufferPool
|
||||
}{
|
||||
{name: "Shared", pool: imem.NewDirtySimplePool()},
|
||||
{name: "Unshared"},
|
||||
} {
|
||||
b.Run(test.name, func(b *testing.B) {
|
||||
b.Run("New", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
|
||||
_ = writer
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("WriteAndFlush", func(b *testing.B) {
|
||||
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(payloadSize)
|
||||
for b.Loop() {
|
||||
if _, err := writer.Write(payload); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("EmptyFlush", func(b *testing.B) {
|
||||
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
if err := writer.Flush(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FullBufferWrite", func(b *testing.B) {
|
||||
writer := newBufWriter(io.Discard, defaultWriteBufSize, test.pool)
|
||||
fullBuffer := make([]byte, defaultWriteBufSize)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(defaultWriteBufSize)
|
||||
for b.Loop() {
|
||||
if _, err := writer.Write(fullBuffer); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecodeGrpcMessage(b *testing.B) {
|
||||
input := "Hello, %E4%B8%96%E7%95%8C"
|
||||
want := "Hello, 世界"
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := decodeGrpcMessage(input)
|
||||
if got != want {
|
||||
b.Fatalf("decodeGrpcMessage(%q) = %s, want %s", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodeGrpcMessage(b *testing.B) {
|
||||
input := "Hello, 世界"
|
||||
want := "Hello, %E4%B8%96%E7%95%8C"
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := encodeGrpcMessage(input)
|
||||
if got != want {
|
||||
b.Fatalf("encodeGrpcMessage(%q) = %s, want %s", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildDataFrame(h http2.FrameHeader, payload []byte) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.Write([]byte{
|
||||
byte(h.Length >> 16),
|
||||
byte(h.Length >> 8),
|
||||
byte(h.Length),
|
||||
byte(h.Type),
|
||||
byte(h.Flags),
|
||||
byte(h.StreamID >> 24),
|
||||
byte(h.StreamID >> 16),
|
||||
byte(h.StreamID >> 8),
|
||||
byte(h.StreamID),
|
||||
})
|
||||
buf.Write(payload)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (s) TestFramer_ParseDataFrame(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wire []byte // from frame header onward
|
||||
wantData []byte
|
||||
wantErr error
|
||||
wantErrDetailSubstr string
|
||||
}{
|
||||
{
|
||||
name: "good_padded",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 6, StreamID: 1, Flags: http2.FlagDataPadded,
|
||||
}, []byte{
|
||||
2, // pad length
|
||||
'f', 'o', 'o', // data
|
||||
0, 0, // padding
|
||||
}),
|
||||
wantData: []byte("foo"),
|
||||
},
|
||||
{
|
||||
name: "good_unpadded",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 3, StreamID: 1, Flags: 0,
|
||||
}, []byte("foo")),
|
||||
wantData: []byte("foo"),
|
||||
},
|
||||
{
|
||||
name: "stream_id_0",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 1, StreamID: 0, Flags: 0,
|
||||
}, []byte{0}),
|
||||
wantErr: http2.ConnectionError(http2.ErrCodeProtocol),
|
||||
wantErrDetailSubstr: "DATA frame with stream ID 0",
|
||||
},
|
||||
{
|
||||
name: "pad_size_bigger_than_payload",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 4, StreamID: 1, Flags: http2.FlagDataPadded,
|
||||
}, []byte{
|
||||
4, // pad length of 4
|
||||
'f', 'o', // data 'fo' is 2 bytes
|
||||
0, // padding 0 is 1 byte.
|
||||
}), // pad length 4 but only 3 bytes for data+padding available in payload after pad length byte
|
||||
wantErr: http2.ConnectionError(http2.ErrCodeProtocol),
|
||||
wantErrDetailSubstr: "pad size larger than data payload",
|
||||
},
|
||||
{
|
||||
name: "padded_zero_data_some_padding",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 3, StreamID: 1, Flags: http2.FlagDataPadded,
|
||||
}, []byte{
|
||||
2, // pad length 2
|
||||
0, 0, // padding
|
||||
}),
|
||||
wantData: []byte{},
|
||||
},
|
||||
{
|
||||
name: "padded_short_payload_reading_pad_flag",
|
||||
wire: buildDataFrame(http2.FrameHeader{
|
||||
Type: http2.FrameData, Length: 0, StreamID: 1, Flags: http2.FlagDataPadded,
|
||||
}, []byte{}),
|
||||
wantErr: io.ErrUnexpectedEOF,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fr := newFramer(bytes.NewBuffer(tc.wire), defaultWriteBufSize, defaultReadBufSize, false, defaultClientMaxHeaderListSize, mem.DefaultBufferPool())
|
||||
f, err := fr.readFrame()
|
||||
|
||||
if err != tc.wantErr {
|
||||
t.Fatalf("readFrame() returned unexpected error: %v, want %v", err, tc.wantErr)
|
||||
}
|
||||
gotErrDetailStr := ""
|
||||
if fr.errDetail != nil {
|
||||
gotErrDetailStr = fr.errDetail.Error()
|
||||
}
|
||||
if !strings.Contains(gotErrDetailStr, tc.wantErrDetailSubstr) {
|
||||
t.Fatalf("errorDetail() returned unexpected error string: %q, want substring %q", gotErrDetailStr, tc.wantErrDetailSubstr)
|
||||
}
|
||||
|
||||
if tc.wantErr != nil {
|
||||
return
|
||||
}
|
||||
df, ok := f.(*parsedDataFrame)
|
||||
if !ok {
|
||||
t.Fatalf("readFrame() returned %T, want *parsedDataFrame", f)
|
||||
}
|
||||
if gotData := df.data.ReadOnlyData(); !bytes.Equal(gotData, tc.wantData) {
|
||||
t.Fatalf("parsedDataFrame.Data() = %q, want %q", gotData, tc.wantData)
|
||||
}
|
||||
df.data.Free()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testReadyReader struct {
|
||||
readyreader.Reader
|
||||
}
|
||||
|
||||
func (t *testReadyReader) Read([]byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (s) TestBufferedReader(t *testing.T) {
|
||||
normalReader := bytes.NewReader(nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reader io.Reader
|
||||
bufSize int
|
||||
enablePooling bool
|
||||
wantTypeOf any
|
||||
}{
|
||||
{
|
||||
name: "bufSize_0",
|
||||
reader: normalReader,
|
||||
bufSize: 0,
|
||||
enablePooling: true,
|
||||
wantTypeOf: (*bytes.Reader)(nil),
|
||||
},
|
||||
{
|
||||
name: "env_var_disabled_normal_reader",
|
||||
reader: normalReader,
|
||||
bufSize: 10,
|
||||
enablePooling: false,
|
||||
wantTypeOf: (*bufio.Reader)(nil),
|
||||
},
|
||||
{
|
||||
name: "env_var_disabled_ready_reader",
|
||||
reader: &testReadyReader{},
|
||||
bufSize: 10,
|
||||
enablePooling: false,
|
||||
wantTypeOf: (*bufio.Reader)(nil),
|
||||
},
|
||||
{
|
||||
name: "env_var_enabled_normal_reader",
|
||||
reader: normalReader,
|
||||
bufSize: 10,
|
||||
enablePooling: true,
|
||||
wantTypeOf: (*bufio.Reader)(nil),
|
||||
},
|
||||
{
|
||||
name: "env_var_enabled_ready_reader",
|
||||
reader: &testReadyReader{},
|
||||
bufSize: 10,
|
||||
enablePooling: true,
|
||||
wantTypeOf: readyreader.NewBuffered(nil, 10, mem.DefaultBufferPool()),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testutils.SetEnvConfig(t, &envconfig.EnableHTTPFramerReadBufferPooling, tt.enablePooling)
|
||||
|
||||
got := bufferedReader(tt.reader, tt.bufSize)
|
||||
|
||||
gotType := reflect.TypeOf(got)
|
||||
wantType := reflect.TypeOf(tt.wantTypeOf)
|
||||
|
||||
if gotType != wantType {
|
||||
t.Errorf("bufferedReader() type = %v, want %v", gotType, wantType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
25
internal/transport/internal/internal.go
Normal file
25
internal/transport/internal/internal.go
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
*
|
||||
* 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 internal contains functionality internal to the transport package.
|
||||
package internal
|
||||
|
||||
// TimeNowFunc is a variable that can be set to override the default behavior of
|
||||
// getting the current time in nanoseconds. It is used in transport code to set
|
||||
// channelz timestamps, and is exposed here for testing purposes.
|
||||
var TimeNowFunc func() int64
|
||||
860
internal/transport/keepalive_test.go
Normal file
860
internal/transport/keepalive_test.go
Normal file
@@ -0,0 +1,860 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2019 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// This file contains tests related to the following proposals:
|
||||
// https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md
|
||||
// https://github.com/grpc/proposal/blob/master/A9-server-side-conn-mgt.md
|
||||
// https://github.com/grpc/proposal/blob/master/A18-tcp-user-timeout.md
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal/channelz"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/syscall"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/testdata"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
const defaultTestShortTimeout = 10 * time.Millisecond
|
||||
|
||||
// TestMaxConnectionIdle tests that a server will send GoAway to an idle
|
||||
// client. An idle client is one who doesn't make any RPC calls for a duration
|
||||
// of MaxConnectionIdle time.
|
||||
func (s) TestMaxConnectionIdle(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
MaxConnectionIdle: 30 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("client.NewStream() failed: %v", err)
|
||||
}
|
||||
stream.Close(io.EOF)
|
||||
|
||||
// Verify the server sends a GoAway to client after MaxConnectionIdle timeout
|
||||
// kicks in.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("context expired before receiving GoAway from the server.")
|
||||
case <-client.GoAway():
|
||||
reason, debugMsg := client.GetGoAwayReason()
|
||||
if reason != GoAwayNoReason {
|
||||
t.Fatalf("GoAwayReason is %v, want %v", reason, GoAwayNoReason)
|
||||
}
|
||||
if !strings.Contains(debugMsg, "max_idle") {
|
||||
t.Fatalf("GoAwayDebugMessage is %v, want %v", debugMsg, "max_idle")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaxConnectionIdleBusyClient tests that a server will not send GoAway to
|
||||
// a busy client.
|
||||
func (s) TestMaxConnectionIdleBusyClient(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
MaxConnectionIdle: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
_, err := client.NewStream(ctx, &CallHdr{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("client.NewStream() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the server does not send a GoAway to client even after MaxConnectionIdle
|
||||
// timeout kicks in.
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
select {
|
||||
case <-client.GoAway():
|
||||
t.Fatalf("A busy client received a GoAway.")
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaxConnectionAge tests that a server will send GoAway after a duration
|
||||
// of MaxConnectionAge.
|
||||
func (s) TestMaxConnectionAge(t *testing.T) {
|
||||
maxConnAge := 100 * time.Millisecond
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
MaxConnectionAge: maxConnAge,
|
||||
MaxConnectionAgeGrace: 10 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
|
||||
t.Fatalf("client.NewStream() failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the server sends a GoAway to client even after client remains idle
|
||||
// for more than MaxConnectionIdle time.
|
||||
select {
|
||||
case <-client.GoAway():
|
||||
reason, debugMsg := client.GetGoAwayReason()
|
||||
if reason != GoAwayNoReason {
|
||||
t.Fatalf("GoAwayReason is %v, want %v", reason, GoAwayNoReason)
|
||||
}
|
||||
if !strings.Contains(debugMsg, "max_age") {
|
||||
t.Fatalf("GoAwayDebugMessage is %v, want %v", debugMsg, "max_age")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timed out before getting a GoAway from the server.")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
defaultWriteBufSize = 32 * 1024
|
||||
defaultReadBufSize = 32 * 1024
|
||||
)
|
||||
|
||||
// TestKeepaliveServerClosesUnresponsiveClient tests that a server closes
|
||||
// the connection with a client that doesn't respond to keepalive pings.
|
||||
//
|
||||
// This test creates a regular net.Conn connection to the server and sends the
|
||||
// clientPreface and the initial Settings frame, and then remains unresponsive.
|
||||
func (s) TestKeepaliveServerClosesUnresponsiveClient(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
Time: 100 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
addr := server.addr()
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatalf("net.Dial(tcp, %v) failed: %v", addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if n, err := conn.Write(clientPreface); err != nil || n != len(clientPreface) {
|
||||
t.Fatalf("conn.Write(clientPreface) failed: n=%v, err=%v", n, err)
|
||||
}
|
||||
framer := newFramer(conn, defaultWriteBufSize, defaultReadBufSize, false, 0, mem.DefaultBufferPool())
|
||||
if err := framer.fr.WriteSettings(http2.Setting{}); err != nil {
|
||||
t.Fatal("framer.WriteSettings(http2.Setting{}) failed:", err)
|
||||
}
|
||||
framer.writer.Flush()
|
||||
|
||||
// We read from the net.Conn till we get an error, which is expected when
|
||||
// the server closes the connection as part of the keepalive logic.
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
b := make([]byte, 24)
|
||||
for {
|
||||
if _, err = conn.Read(b); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Server waits for KeepaliveParams.Time seconds before sending out a ping,
|
||||
// and then waits for KeepaliveParams.Timeout for a ping ack.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != io.EOF {
|
||||
t.Fatalf("client.Read(_) = _,%v, want io.EOF", err)
|
||||
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Test timed out before server closed the connection.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerWithResponsiveClient tests that a server doesn't close
|
||||
// the connection with a client that responds to keepalive pings.
|
||||
func (s) TestKeepaliveServerWithResponsiveClient(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
Time: 100 * time.Millisecond,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, copts)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Give keepalive logic some time by sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func channelzSubChannel(t *testing.T) *channelz.SubChannel {
|
||||
ch := channelz.RegisterChannel(nil, "test chan")
|
||||
sc := channelz.RegisterSubChannel(ch, "test subchan")
|
||||
t.Cleanup(func() {
|
||||
channelz.RemoveEntry(sc.ID)
|
||||
channelz.RemoveEntry(ch.ID)
|
||||
})
|
||||
return sc
|
||||
}
|
||||
|
||||
// TestKeepaliveClientClosesUnresponsiveServer creates a server which does not
|
||||
// respond to keepalive pings, and makes sure that the client closes the
|
||||
// transport once the keepalive logic kicks in. Here, we set the
|
||||
// `PermitWithoutStream` parameter to true which ensures that the keepalive
|
||||
// logic is running even without any active streams.
|
||||
func (s) TestKeepaliveClientClosesUnresponsiveServer(t *testing.T) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
ChannelzParent: channelzSubChannel(t),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 10 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
|
||||
server.setPingAck(false)
|
||||
defer cancel()
|
||||
defer client.Close(fmt.Errorf("closed manually by test"))
|
||||
|
||||
conn, ok := <-connCh
|
||||
if !ok {
|
||||
t.Fatalf("Server didn't return connection object")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if err := pollForStreamCreationError(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveClientOpenWithUnresponsiveServer creates a server which does
|
||||
// not respond to keepalive pings, and makes sure that the client does not
|
||||
// close the transport. Here, we do not set the `PermitWithoutStream` parameter
|
||||
// to true which ensures that the keepalive logic is turned off without any
|
||||
// active streams, and therefore the transport stays open.
|
||||
func (s) TestKeepaliveClientOpenWithUnresponsiveServer(t *testing.T) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
ChannelzParent: channelzSubChannel(t),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 10 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
|
||||
server.setPingAck(false)
|
||||
defer cancel()
|
||||
defer client.Close(fmt.Errorf("closed manually by test"))
|
||||
|
||||
conn, ok := <-connCh
|
||||
if !ok {
|
||||
t.Fatalf("Server didn't return connection object")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Give keepalive some time.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveClientClosesWithActiveStreams creates a responsive server and
|
||||
// then stops responding to keepalive pings. It makes sure that the client
|
||||
// closes the transport even when there is an active stream.
|
||||
func (s) TestKeepaliveClientClosesWithActiveStreams(t *testing.T) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
ChannelzParent: channelzSubChannel(t),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 10 * time.Millisecond,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpControllablePingServer(t, copts, connCh)
|
||||
|
||||
defer cancel()
|
||||
defer client.Close(fmt.Errorf("closed manually by test"))
|
||||
|
||||
conn, ok := <-connCh
|
||||
if !ok {
|
||||
t.Fatalf("Server didn't return connection object")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
// Create a stream, but send no data on it.
|
||||
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
|
||||
// Now that we have an active stream and verified the connection is stable,
|
||||
// we want to simulate a "no-ping" server.
|
||||
server.setPingAck(false)
|
||||
|
||||
if err := pollForStreamCreationError(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveClientStaysHealthyWithResponsiveServer creates a server which
|
||||
// responds to keepalive pings, and makes sure than a client transport stays
|
||||
// healthy without any active streams.
|
||||
func (s) TestKeepaliveClientStaysHealthyWithResponsiveServer(t *testing.T) {
|
||||
server, client, cancel := setUpWithOptions(t, 0,
|
||||
&ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: 50 * time.Millisecond,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
},
|
||||
normal,
|
||||
ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 55 * time.Millisecond,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}})
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Give keepalive some time.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveClientFrequency creates a server which expects at most 1 client
|
||||
// ping for every 100 ms, while the client is configured to send a ping
|
||||
// every 50 ms. So, this configuration should end up with the client
|
||||
// transport being closed. But we had a bug wherein the client was sending one
|
||||
// ping every [Time+Timeout] instead of every [Time] period, and this test
|
||||
// explicitly makes sure the fix works and the client sends a ping every [Time]
|
||||
// period.
|
||||
func (s) TestKeepaliveClientFrequency(t *testing.T) {
|
||||
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
|
||||
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: 100 * time.Millisecond,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 50 * time.Millisecond,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if err := waitForGoAwayTooManyPings(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerEnforcementWithAbusiveClientNoRPC verifies that the
|
||||
// server closes a client transport when it sends too many keepalive pings
|
||||
// (when there are no active streams), based on the configured
|
||||
// EnforcementPolicy.
|
||||
func (s) TestKeepaliveServerEnforcementWithAbusiveClientNoRPC(t *testing.T) {
|
||||
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
|
||||
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: time.Second,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 20 * time.Millisecond,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if err := waitForGoAwayTooManyPings(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerEnforcementWithAbusiveClientWithRPC verifies that the
|
||||
// server closes a client transport when it sends too many keepalive pings
|
||||
// (even when there is an active stream), based on the configured
|
||||
// EnforcementPolicy.
|
||||
func (s) TestKeepaliveServerEnforcementWithAbusiveClientWithRPC(t *testing.T) {
|
||||
grpctest.ExpectError("Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII \"too_many_pings\"")
|
||||
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: time.Second,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 50 * time.Millisecond,
|
||||
Timeout: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
|
||||
if err := waitForGoAwayTooManyPings(client); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerEnforcementWithObeyingClientNoRPC verifies that the
|
||||
// server does not close a client transport (with no active streams) which
|
||||
// sends keepalive pings in accordance to the configured keepalive
|
||||
// EnforcementPolicy.
|
||||
func (s) TestKeepaliveServerEnforcementWithObeyingClientNoRPC(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: 40 * time.Millisecond,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 50 * time.Millisecond,
|
||||
Timeout: time.Second,
|
||||
PermitWithoutStream: true,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Sleep for client to send ~10 keepalive pings.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// Verify that the server does not close the client transport.
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerEnforcementWithObeyingClientWithRPC verifies that the
|
||||
// server does not close a client transport (with active streams) which
|
||||
// sends keepalive pings in accordance to the configured keepalive
|
||||
// EnforcementPolicy.
|
||||
func (s) TestKeepaliveServerEnforcementWithObeyingClientWithRPC(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: 40 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 50 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, suspended, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
|
||||
// Give keepalive enough time.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKeepaliveServerEnforcementWithDormantKeepaliveOnClient verifies that the
|
||||
// server does not closes a client transport, which has been configured to send
|
||||
// more pings than allowed by the server's EnforcementPolicy. This client
|
||||
// transport does not have any active streams and `PermitWithoutStream` is set
|
||||
// to false. This should ensure that the keepalive functionality on the client
|
||||
// side enters a dormant state.
|
||||
func (s) TestKeepaliveServerEnforcementWithDormantKeepaliveOnClient(t *testing.T) {
|
||||
serverConfig := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepalivePolicy: keepalive.EnforcementPolicy{
|
||||
MinTime: 100 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
clientOptions := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: 10 * time.Millisecond,
|
||||
Timeout: 10 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
server, client, cancel := setUpWithOptions(t, 0, serverConfig, normal, clientOptions)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// No active streams on the client. Give keepalive enough time.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if err := checkForHealthyStream(client); err != nil {
|
||||
t.Fatalf("Stream creation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTCPUserTimeout tests that the TCP_USER_TIMEOUT socket option is set to
|
||||
// the keepalive timeout, as detailed in proposal A18.
|
||||
func (s) TestTCPUserTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
tls bool
|
||||
time time.Duration
|
||||
timeout time.Duration
|
||||
clientWantTimeout time.Duration
|
||||
serverWantTimeout time.Duration
|
||||
}{
|
||||
{
|
||||
false,
|
||||
10 * time.Second,
|
||||
10 * time.Second,
|
||||
10 * 1000 * time.Millisecond,
|
||||
10 * 1000 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
20 * 1000 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
false,
|
||||
infinity,
|
||||
infinity,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
{
|
||||
true,
|
||||
10 * time.Second,
|
||||
10 * time.Second,
|
||||
10 * 1000 * time.Millisecond,
|
||||
10 * 1000 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
true,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
20 * 1000 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
true,
|
||||
infinity,
|
||||
infinity,
|
||||
0,
|
||||
0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
sopts := &ServerConfig{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ServerParameters{
|
||||
Time: tt.time,
|
||||
Timeout: tt.timeout,
|
||||
},
|
||||
}
|
||||
|
||||
copts := ConnectOptions{
|
||||
BufferPool: mem.DefaultBufferPool(),
|
||||
KeepaliveParams: keepalive.ClientParameters{
|
||||
Time: tt.time,
|
||||
Timeout: tt.timeout,
|
||||
},
|
||||
}
|
||||
|
||||
if tt.tls {
|
||||
copts.TransportCredentials = makeTLSCreds(t, "x509/client1_cert.pem", "x509/client1_key.pem", "x509/server_ca_cert.pem")
|
||||
sopts.Credentials = makeTLSCreds(t, "x509/server1_cert.pem", "x509/server1_key.pem", "x509/client_ca_cert.pem")
|
||||
|
||||
}
|
||||
|
||||
server, client, cancel := setUpWithOptions(
|
||||
t,
|
||||
0,
|
||||
sopts,
|
||||
normal,
|
||||
copts,
|
||||
)
|
||||
defer func() {
|
||||
client.Close(fmt.Errorf("closed manually by test"))
|
||||
server.stop()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
var sc *http2Server
|
||||
var srawConn net.Conn
|
||||
// Wait until the server transport is setup.
|
||||
for {
|
||||
server.mu.Lock()
|
||||
if len(server.conns) == 0 {
|
||||
server.mu.Unlock()
|
||||
time.Sleep(time.Millisecond)
|
||||
continue
|
||||
}
|
||||
for k := range server.conns {
|
||||
var ok bool
|
||||
sc, ok = k.(*http2Server)
|
||||
if !ok {
|
||||
t.Fatalf("Failed to convert %v to *http2Server", k)
|
||||
}
|
||||
srawConn = server.conns[k]
|
||||
}
|
||||
server.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("client.NewStream() failed: %v", err)
|
||||
}
|
||||
stream.Close(io.EOF)
|
||||
|
||||
// check client TCP user timeout only when non TLS
|
||||
// TODO : find a way to get the underlying conn for client when TLS
|
||||
if !tt.tls {
|
||||
cltOpt, err := syscall.GetTCPUserTimeout(client.conn)
|
||||
if err != nil {
|
||||
t.Fatalf("syscall.GetTCPUserTimeout() failed: %v", err)
|
||||
}
|
||||
if cltOpt < 0 {
|
||||
t.Skipf("skipping test on unsupported environment")
|
||||
}
|
||||
if gotTimeout := time.Duration(cltOpt) * time.Millisecond; gotTimeout != tt.clientWantTimeout {
|
||||
t.Fatalf("syscall.GetTCPUserTimeout() = %d, want %d", gotTimeout, tt.clientWantTimeout)
|
||||
}
|
||||
}
|
||||
scConn := sc.conn
|
||||
if tt.tls {
|
||||
if _, ok := sc.conn.(*net.TCPConn); ok {
|
||||
t.Fatalf("sc.conn is should have wrapped conn with TLS")
|
||||
}
|
||||
scConn = srawConn
|
||||
}
|
||||
// verify the type of scConn (on which TCP user timeout will be got)
|
||||
if _, ok := scConn.(*net.TCPConn); !ok {
|
||||
t.Fatalf("server underlying conn is of type %T, want net.TCPConn", scConn)
|
||||
}
|
||||
srvOpt, err := syscall.GetTCPUserTimeout(scConn)
|
||||
if err != nil {
|
||||
t.Fatalf("syscall.GetTCPUserTimeout() failed: %v", err)
|
||||
}
|
||||
if gotTimeout := time.Duration(srvOpt) * time.Millisecond; gotTimeout != tt.serverWantTimeout {
|
||||
t.Fatalf("syscall.GetTCPUserTimeout() = %d, want %d", gotTimeout, tt.serverWantTimeout)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func makeTLSCreds(t *testing.T, certPath, keyPath, rootsPath string) credentials.TransportCredentials {
|
||||
cert, err := tls.LoadX509KeyPair(testdata.Path(certPath), testdata.Path(keyPath))
|
||||
if err != nil {
|
||||
t.Fatalf("tls.LoadX509KeyPair(%q, %q) failed: %v", certPath, keyPath, err)
|
||||
}
|
||||
b, err := os.ReadFile(testdata.Path(rootsPath))
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile(%q) failed: %v", rootsPath, err)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM(b) {
|
||||
t.Fatal("failed to append certificates")
|
||||
}
|
||||
return credentials.NewTLS(&tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: roots,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
}
|
||||
|
||||
// checkForHealthyStream attempts to create a stream and return error if any.
|
||||
// The stream created is closed right after to avoid any leakages.
|
||||
func checkForHealthyStream(client *http2Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
stream, err := client.NewStream(ctx, &CallHdr{}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream.Close(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pollForStreamCreationError(client *http2Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
for {
|
||||
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return fmt.Errorf("test timed out before stream creation returned an error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForGoAwayTooManyPings waits for client to receive a GoAwayTooManyPings
|
||||
// from server. It also asserts that stream creation fails after receiving a
|
||||
// GoAway.
|
||||
func waitForGoAwayTooManyPings(client *http2Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
select {
|
||||
case <-client.GoAway():
|
||||
if reason, _ := client.GetGoAwayReason(); reason != GoAwayTooManyPings {
|
||||
return fmt.Errorf("goAwayReason is %v, want %v", reason, GoAwayTooManyPings)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("test timed out before getting GoAway with reason:GoAwayTooManyPings from server")
|
||||
}
|
||||
|
||||
if _, err := client.NewStream(ctx, &CallHdr{}, nil); err == nil {
|
||||
return fmt.Errorf("stream creation succeeded after receiving a GoAway from the server")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
40
internal/transport/logging.go
Normal file
40
internal/transport/logging.go
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 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 (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("transport")
|
||||
|
||||
func prefixLoggerForServerTransport(p *http2Server) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[server-transport %p] ", p))
|
||||
}
|
||||
|
||||
func prefixLoggerForServerHandlerTransport(p *serverHandlerTransport) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[server-handler-transport %p] ", p))
|
||||
}
|
||||
|
||||
func prefixLoggerForClientTransport(p *http2Client) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf("[client-transport %p] ", p))
|
||||
}
|
||||
46
internal/transport/networktype/networktype.go
Normal file
46
internal/transport/networktype/networktype.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 networktype declares the network type to be used in the default
|
||||
// dialer. Attribute of a resolver.Address.
|
||||
package networktype
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
// keyType is the key to use for storing State in Attributes.
|
||||
type keyType string
|
||||
|
||||
const key = keyType("grpc.internal.transport.networktype")
|
||||
|
||||
// Set returns a copy of the provided address with attributes containing networkType.
|
||||
func Set(address resolver.Address, networkType string) resolver.Address {
|
||||
address.Attributes = address.Attributes.WithValue(key, networkType)
|
||||
return address
|
||||
}
|
||||
|
||||
// Get returns the network type in the resolver.Address and true, or "", false
|
||||
// if not present.
|
||||
func Get(address resolver.Address) (string, bool) {
|
||||
v := address.Attributes.Value(key)
|
||||
if v == nil {
|
||||
return "", false
|
||||
}
|
||||
return v.(string), true
|
||||
}
|
||||
116
internal/transport/proxy.go
Normal file
116
internal/transport/proxy.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2017 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 (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/proxyattributes"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const proxyAuthHeaderKey = "Proxy-Authorization"
|
||||
|
||||
// To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader.
|
||||
// It's possible that this reader reads more than what's need for the response
|
||||
// and stores those bytes in the buffer. bufConn wraps the original net.Conn
|
||||
// and the bufio.Reader to make sure we don't lose the bytes in the buffer.
|
||||
type bufConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c *bufConn) Read(b []byte) (int, error) {
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
func basicAuth(username, password string) string {
|
||||
auth := username + ":" + password
|
||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
|
||||
func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, grpcUA string, opts proxyattributes.Options) (_ net.Conn, err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
req := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: opts.ConnectAddr},
|
||||
Header: map[string][]string{"User-Agent": {grpcUA}},
|
||||
}
|
||||
if user := opts.User; user != nil {
|
||||
u := user.Username()
|
||||
p, _ := user.Password()
|
||||
req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))
|
||||
}
|
||||
if err := sendHTTPRequest(ctx, req, conn); err != nil {
|
||||
return nil, fmt.Errorf("failed to write the HTTP request: %v", err)
|
||||
}
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(r, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading server HTTP response: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
dump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump)
|
||||
}
|
||||
// The buffer could contain extra bytes from the target server, so we can't
|
||||
// discard it. However, in many cases where the server waits for the client
|
||||
// to send the first message (e.g. when TLS is being used), the buffer will
|
||||
// be empty, so we can avoid the overhead of reading through this buffer.
|
||||
if r.Buffered() != 0 {
|
||||
return &bufConn{Conn: conn, r: r}, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// proxyDial establishes a TCP connection to the specified address and performs an HTTP CONNECT handshake.
|
||||
func proxyDial(ctx context.Context, addr resolver.Address, grpcUA string, opts proxyattributes.Options) (net.Conn, error) {
|
||||
conn, err := internal.NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", addr.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doHTTPConnectHandshake(ctx, conn, grpcUA, opts)
|
||||
}
|
||||
|
||||
func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error {
|
||||
req = req.WithContext(ctx)
|
||||
if err := req.Write(conn); err != nil {
|
||||
return fmt.Errorf("failed to write the HTTP request: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
441
internal/transport/proxy_ext_test.go
Normal file
441
internal/transport/proxy_ext_test.go
Normal file
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/resolver/delegatingresolver"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/proxyserver"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
)
|
||||
|
||||
const defaultTestTimeout = 10 * time.Second
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func startBackendServer(t *testing.T) *stubserver.StubServer {
|
||||
t.Helper()
|
||||
backend := &stubserver.StubServer{
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) { return &testpb.Empty{}, nil },
|
||||
}
|
||||
if err := backend.StartServer(); err != nil {
|
||||
t.Fatalf("failed to start backend: %v", err)
|
||||
}
|
||||
t.Logf("Started TestService backend at: %q", backend.Address)
|
||||
t.Cleanup(backend.Stop)
|
||||
return backend
|
||||
}
|
||||
|
||||
func isIPAddr(addr string) bool {
|
||||
_, err := netip.ParseAddr(addr)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func overrideTestHTTPSProxy(t *testing.T, proxyAddr string) {
|
||||
t.Helper()
|
||||
hpfe := func(*http.Request) (*url.URL, error) {
|
||||
return &url.URL{
|
||||
Scheme: "https",
|
||||
Host: proxyAddr,
|
||||
}, nil
|
||||
}
|
||||
originalhpfe := delegatingresolver.HTTPSProxyFromEnvironment
|
||||
delegatingresolver.HTTPSProxyFromEnvironment = hpfe
|
||||
t.Cleanup(func() { delegatingresolver.HTTPSProxyFromEnvironment = originalhpfe })
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.Dial is performed using a proxy with the
|
||||
// default resolver in the target URI. The test verifies that the connection is
|
||||
// established to the proxy server, sends the unresolved target URI in the HTTP
|
||||
// CONNECT request and is successfully connected to the backend server.
|
||||
func (s) TestGRPCDialWithProxy(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := host, "localhost"; got != want {
|
||||
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
// Use "localhost:<port>" to verify the proxy address is handled
|
||||
// correctly by the delegating resolver and connects to the proxy server
|
||||
// correctly even when unresolved.
|
||||
pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pServer.Addr))
|
||||
|
||||
overrideTestHTTPSProxy(t, pAddr)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.Dial(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.Dial(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where `grpc.Dial` is performed with a proxy and the "dns"
|
||||
// scheme for the target. The test verifies that the proxy URI is correctly
|
||||
// resolved and that the target URI resolution on the client preserves the
|
||||
// original behavior of `grpc.Dial`. It also ensures that a connection is
|
||||
// established to the proxy server, with the resolved target URI sent in the
|
||||
// HTTP CONNECT request, successfully connecting to the backend server.
|
||||
func (s) TestGRPCDialWithDNSAndProxy(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := isIPAddr(host), true; got != want {
|
||||
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
overrideTestHTTPSProxy(t, pServer.Addr)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.Dial("dns:///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.Dial(%s) failed: %v", "dns:///"+unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where `grpc.NewClient` is used with the default DNS
|
||||
// resolver for the target URI and a proxy is configured. The test verifies
|
||||
// that the client resolves proxy URI, connects to the proxy server, sends the
|
||||
// unresolved target URI in the HTTP CONNECT request, and successfully
|
||||
// establishes a connection to the backend server.
|
||||
func (s) TestNewClientWithProxy(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := host, "localhost"; got != want {
|
||||
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
// Use "localhost:<port>" to verify the proxy address is handled
|
||||
// correctly by the delegating resolver and connects to the proxy server
|
||||
// correctly even when unresolved.
|
||||
pAddr := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, pServer.Addr))
|
||||
|
||||
overrideTestHTTPSProxy(t, pAddr)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.NewClient is used with a custom target URI
|
||||
// scheme and a proxy is configured. The test verifies that the client
|
||||
// successfully connects to the proxy server, resolves the proxy URI correctly,
|
||||
// includes the resolved target URI in the HTTP CONNECT request, and
|
||||
// establishes a connection to the backend server.
|
||||
func (s) TestNewClientWithProxyAndCustomResolver(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := isIPAddr(host), true; got != want {
|
||||
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
overrideTestHTTPSProxy(t, pServer.Addr)
|
||||
|
||||
// Create and update a custom resolver for target URI.
|
||||
targetResolver := manual.NewBuilderWithScheme("test")
|
||||
resolver.Register(targetResolver)
|
||||
targetResolver.InitialState(resolver.State{Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: backend.Address}}}}})
|
||||
|
||||
// Dial to the proxy server.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(targetResolver.Scheme()+":///"+unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", targetResolver.Scheme()+":///"+unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.NewClient is used with the default "dns"
|
||||
// resolver and the dial option grpc.WithLocalDNSResolution() is set,
|
||||
// enabling target resolution on the client. The test verifies that target
|
||||
// resolution happens on the client by sending resolved target URI in HTTP
|
||||
// CONNECT request, the proxy URI is resolved correctly, and the connection is
|
||||
// successfully established with the backend server through the proxy.
|
||||
func (s) TestNewClientWithProxyAndTargetResolutionEnabled(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got, want := isIPAddr(host), true; got != want {
|
||||
t.Errorf("isIPAddr(%q) = %t, want = %t", host, got, want)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
overrideTestHTTPSProxy(t, pServer.Addr)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithLocalDNSResolution(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.NewClient is used with grpc.WithNoProxy() set,
|
||||
// explicitly disabling proxy usage. The test verifies that the client does not
|
||||
// dial the proxy but directly connects to the backend server. It also checks
|
||||
// that the proxy resolution function is not called and that the proxy server
|
||||
// never receives a connection request.
|
||||
func (s) TestNewClientWithNoProxy(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") }
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
overrideTestHTTPSProxy(t, pServer.Addr)
|
||||
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithNoProxy(), // Disable proxy.
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(unresolvedTargetURI, dopts...)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create a test service client and make an RPC call.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.NewClient is used with grpc.WithContextDialer()
|
||||
// set. The test verifies that the client bypasses proxy dialing and uses the
|
||||
// custom dialer instead. It ensures that the proxy server is never dialed, the
|
||||
// proxy resolution function is not triggered, and the custom dialer is invoked
|
||||
// as expected.
|
||||
func (s) TestNewClientWithContextDialer(t *testing.T) {
|
||||
backend := startBackendServer(t)
|
||||
unresolvedTargetURI := fmt.Sprintf("localhost:%d", testutils.ParsePort(t, backend.Address))
|
||||
reqCheck := func(_ *http.Request) { t.Error("proxy server should not have received a Connect request") }
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
overrideTestHTTPSProxy(t, pServer.Addr)
|
||||
|
||||
// Create a custom dialer that directly dials the backend.
|
||||
customDialer := func(_ context.Context, unresolvedTargetURI string) (net.Conn, error) {
|
||||
return net.Dial("tcp", unresolvedTargetURI)
|
||||
}
|
||||
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithContextDialer(customDialer), // Use a custom dialer.
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(unresolvedTargetURI, dopts...)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where grpc.NewClient is used with the default DNS resolver
|
||||
// for targetURI and a proxy. The test verifies that the client connects to the
|
||||
// proxy server, sends the unresolved target URI in the HTTP CONNECT request,
|
||||
// and successfully connects to the backend. Additionally, it checks that the
|
||||
// correct user information is included in the Proxy-Authorization header of
|
||||
// the CONNECT request. The test also ensures that target resolution does not
|
||||
// happen on the client.
|
||||
func (s) TestBasicAuthInNewClientWithProxy(t *testing.T) {
|
||||
unresolvedTargetURI := "example.test"
|
||||
const (
|
||||
user = "notAUser"
|
||||
password = "notAPassword"
|
||||
)
|
||||
proxyCalled := false
|
||||
reqCheck := func(req *http.Request) {
|
||||
proxyCalled = true
|
||||
if got, want := req.URL.Host, "example.test:443"; got != want {
|
||||
t.Errorf(" Unexpected request host: %s, want = %s ", got, want)
|
||||
}
|
||||
wantProxyAuthStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
|
||||
if got := req.Header.Get("Proxy-Authorization"); got != wantProxyAuthStr {
|
||||
gotDecoded, err := base64.StdEncoding.DecodeString(got)
|
||||
if err != nil {
|
||||
t.Errorf("failed to decode Proxy-Authorization header: %v", err)
|
||||
}
|
||||
wantDecoded, _ := base64.StdEncoding.DecodeString(wantProxyAuthStr)
|
||||
t.Errorf("unexpected auth %q (%q), want %q (%q)", got, gotDecoded, wantProxyAuthStr, wantDecoded)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, false)
|
||||
|
||||
t.Setenv("HTTPS_PROXY", user+":"+password+"@"+pServer.Addr)
|
||||
|
||||
// Use the httpproxy package functions instead of `http.ProxyFromEnvironment`
|
||||
// because the latter reads proxy-related environment variables only once at
|
||||
// initialization. This behavior causes issues when running test multiple
|
||||
// times, as changes to environment variables during tests would be ignored.
|
||||
// By using `httpproxy.FromEnvironment()`, we ensure proxy settings are read dynamically.
|
||||
origHTTPSProxyFromEnvironment := delegatingresolver.HTTPSProxyFromEnvironment
|
||||
delegatingresolver.HTTPSProxyFromEnvironment = func(req *http.Request) (*url.URL, error) {
|
||||
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
|
||||
}
|
||||
defer func() {
|
||||
delegatingresolver.HTTPSProxyFromEnvironment = origHTTPSProxyFromEnvironment
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.NewClient(unresolvedTargetURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", unresolvedTargetURI, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Send an empty RPC to the backend through the proxy.
|
||||
client := testgrpc.NewTestServiceClient(conn)
|
||||
client.EmptyCall(ctx, &testpb.Empty{})
|
||||
|
||||
if !proxyCalled {
|
||||
t.Fatalf("Proxy not connected")
|
||||
}
|
||||
}
|
||||
105
internal/transport/proxy_test.go
Normal file
105
internal/transport/proxy_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
//go:build !race
|
||||
// +build !race
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright 2017 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 (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal/proxyattributes"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/proxyserver"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
func (s) TestHTTPConnectWithServerHello(t *testing.T) {
|
||||
serverMessage := []byte("server-hello")
|
||||
blis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
reqCheck := func(req *http.Request) {
|
||||
if req.Method != http.MethodConnect {
|
||||
t.Errorf("unexpected Method %q, want %q", req.Method, http.MethodConnect)
|
||||
}
|
||||
host, _, err := net.SplitHostPort(req.URL.Host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
pServer := proxyserver.New(t, reqCheck, true)
|
||||
|
||||
msg := []byte{4, 3, 5, 2}
|
||||
recvBuf := make([]byte, len(msg))
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
in, err := blis.Accept()
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
in.Write(serverMessage)
|
||||
in.Read(recvBuf)
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
// Dial to proxy server.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
c, err := proxyDial(ctx, resolver.Address{Addr: pServer.Addr}, "test", proxyattributes.Options{ConnectAddr: blis.Addr().String()})
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP connect Dial failed: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
c.SetDeadline(time.Now().Add(defaultTestTimeout))
|
||||
|
||||
// Send msg on the connection.
|
||||
c.Write(msg)
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("Failed to accept: %v", err)
|
||||
}
|
||||
|
||||
// Check received msg.
|
||||
if string(recvBuf) != string(msg) {
|
||||
t.Fatalf("Received msg: %v, want %v", recvBuf, msg)
|
||||
}
|
||||
|
||||
if len(serverMessage) > 0 {
|
||||
gotServerMessage := make([]byte, len(serverMessage))
|
||||
if _, err := c.Read(gotServerMessage); err != nil {
|
||||
t.Errorf("Got error while reading message from server: %v", err)
|
||||
return
|
||||
}
|
||||
if string(gotServerMessage) != string(serverMessage) {
|
||||
t.Errorf("Message from server: %v, want %v", gotServerMessage, serverMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
internal/transport/readyreader/raw_conn_linux.go
Normal file
39
internal/transport/readyreader/raw_conn_linux.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
*
|
||||
* 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
|
||||
|
||||
import "syscall"
|
||||
|
||||
func isRawConnSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// sysRead uses the standard syscall package rather than the modern unix package
|
||||
// to avoid triggering the race detector. Because both packages perform sync
|
||||
// operations on a local variable to satisfy the race detector, mixing them
|
||||
// for read and write syscalls causes data races. We use syscall here to remain
|
||||
// consistent with net.Conn implementations in standard library.
|
||||
func sysRead(fd uintptr, p []byte) (int, error) {
|
||||
return syscall.Read(int(fd), p)
|
||||
}
|
||||
|
||||
// wouldBlock checks standard Unix non-blocking errors.
|
||||
func wouldBlock(err error) bool {
|
||||
return err == syscall.EAGAIN || err == syscall.EWOULDBLOCK
|
||||
}
|
||||
35
internal/transport/readyreader/raw_conn_nonlinux.go
Normal file
35
internal/transport/readyreader/raw_conn_nonlinux.go
Normal file
@@ -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 readyreader
|
||||
|
||||
func isRawConnSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// sysRead is not implemented. Support can be added in the future if necessary.
|
||||
func sysRead(uintptr, []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(error) bool {
|
||||
panic("RawConn functionality is not implemented for non-unix platforms.")
|
||||
}
|
||||
253
internal/transport/readyreader/ready_reader.go
Normal file
253
internal/transport/readyreader/ready_reader.go
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
*
|
||||
* 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 provides utilities to perform non-memory-pinning reads.
|
||||
package readyreader
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"google.golang.org/grpc/mem"
|
||||
)
|
||||
|
||||
// Reader is an optional interface that can be implemented by [net.Conn]
|
||||
// implementations to enable gRPC to perform non-memory-pinning reads.
|
||||
type Reader 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
|
||||
// The following fields are stored as field to avoid heap allocations.
|
||||
state readState
|
||||
doRead func(fd uintptr) bool
|
||||
}
|
||||
|
||||
type readState struct {
|
||||
// Request params.
|
||||
bufSize int
|
||||
pool mem.BufferPool
|
||||
|
||||
// Response params.
|
||||
readError error
|
||||
bytesRead int
|
||||
buf *[]byte
|
||||
}
|
||||
|
||||
// NewNonBlocking returns a ReadyReader if the passed reader supports
|
||||
// non-memory-pinning reads, else nil.
|
||||
func NewNonBlocking(r io.Reader) Reader {
|
||||
if rr, ok := r.(Reader); ok {
|
||||
return rr
|
||||
}
|
||||
if !isRawConnSupported() {
|
||||
return nil
|
||||
}
|
||||
// We restrict the types before asserting syscall.Conn. The credentials
|
||||
// package may return a wrapper that implements syscall.Conn by embedding
|
||||
// both the raw connection and the encrypted connection. If the code
|
||||
// attempts to read directly from the raw syscall.RawConn, it would read
|
||||
// encrypted data.
|
||||
switch r.(type) {
|
||||
case *net.TCPConn, *net.UDPConn, *net.UnixConn, *net.IPConn:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
sysConn, ok := r.(syscall.Conn)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
raw, err := sysConn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
rr := &nonBlockingReader{raw: raw}
|
||||
rr.doRead = func(fd uintptr) bool {
|
||||
s := &rr.state
|
||||
|
||||
s.buf = s.pool.Get(s.bufSize)
|
||||
s.bytesRead, s.readError = sysRead(fd, *s.buf)
|
||||
|
||||
if s.readError != nil {
|
||||
s.pool.Put(s.buf)
|
||||
s.buf = nil
|
||||
}
|
||||
return !wouldBlock(s.readError)
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
func (c *nonBlockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
|
||||
c.state = readState{
|
||||
pool: pool,
|
||||
bufSize: bufSize,
|
||||
}
|
||||
err := c.raw.Read(c.doRead)
|
||||
|
||||
buf := c.state.buf
|
||||
n := c.state.bytesRead
|
||||
readErr := c.state.readError
|
||||
c.state = readState{}
|
||||
|
||||
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
|
||||
}
|
||||
if n == 0 {
|
||||
// syscall.Read doesn't consider a graceful socket closure to be an
|
||||
// error condition, but Go's io.Reader expects an EOF error.
|
||||
pool.Put(buf)
|
||||
return nil, 0, io.EOF
|
||||
}
|
||||
return buf, n, nil
|
||||
}
|
||||
|
||||
type blockingReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (c *blockingReader) ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error) {
|
||||
buf := pool.Get(bufSize)
|
||||
n, err := c.reader.Read(*buf)
|
||||
if err != nil {
|
||||
pool.Put(buf)
|
||||
return nil, 0, err
|
||||
}
|
||||
return buf, n, nil
|
||||
}
|
||||
|
||||
// New detects if [syscall.RawConn] is available for non-memory-pinning reads.
|
||||
// If [syscall.RawConn] is unavailable, it falls back to using the simpler
|
||||
// [io.Reader] interface for reads.
|
||||
func New(r io.Reader) Reader {
|
||||
if r := NewNonBlocking(r); r != nil {
|
||||
return r
|
||||
}
|
||||
return &blockingReader{reader: r}
|
||||
}
|
||||
|
||||
// bufReadyReader implements buffering for a ReadyReader object.
|
||||
// A new bufReadyReader is created by calling [NewBuffered].
|
||||
type bufReadyReader struct {
|
||||
buf *[]byte
|
||||
pool mem.BufferPool
|
||||
bufSize int
|
||||
rd Reader // reader provided by the caller
|
||||
r, w int // buf read and write positions
|
||||
err error
|
||||
constPool constBufferPool // stored as a field to avoid heap allocations.
|
||||
}
|
||||
|
||||
// NewBuffered returns a new [io.Reader] with a buffer of the specified size
|
||||
// which is allocated from the provided pool.
|
||||
func NewBuffered(rd Reader, size int, pool mem.BufferPool) io.Reader {
|
||||
return &bufReadyReader{
|
||||
rd: rd,
|
||||
pool: pool,
|
||||
bufSize: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bufReadyReader) readErr() error {
|
||||
err := b.err
|
||||
b.err = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *bufReadyReader) buffered() int { return b.w - b.r }
|
||||
|
||||
// Read reads data into p. It returns the number of bytes read into p. The
|
||||
// bytes are taken from at most one Read on the underlying [ReadyReader],
|
||||
// hence n may be less than len(p). If the underlying [ReadyReader] can return
|
||||
// a non-zero count with io.EOF, then this Read method can do so as well; see
|
||||
// the [io.Reader] docs.
|
||||
func (b *bufReadyReader) Read(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
if n == 0 {
|
||||
if b.buffered() > 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, b.readErr()
|
||||
}
|
||||
if b.r == b.w {
|
||||
if b.err != nil {
|
||||
return 0, b.readErr()
|
||||
}
|
||||
if len(p) >= b.bufSize {
|
||||
// Large read, empty buffer.
|
||||
// Read directly into p to avoid copy.
|
||||
b.constPool.buffer = p
|
||||
_, n, b.err = b.rd.ReadOnReady(len(p), &b.constPool)
|
||||
return n, b.readErr()
|
||||
}
|
||||
// One read.
|
||||
b.r = 0
|
||||
b.w = 0
|
||||
b.buf, n, b.err = b.rd.ReadOnReady(b.bufSize, b.pool)
|
||||
if n == 0 {
|
||||
if b.buf != nil {
|
||||
b.pool.Put(b.buf)
|
||||
b.buf = nil
|
||||
}
|
||||
return 0, b.readErr()
|
||||
}
|
||||
b.w += n
|
||||
}
|
||||
|
||||
// copy as much as we can
|
||||
// b.buf must be non-nil since b.r != b.w.
|
||||
buf := *b.buf
|
||||
n = copy(p, buf[b.r:b.w])
|
||||
b.r += n
|
||||
if b.r == b.w {
|
||||
// Consumed entire buffer, release it.
|
||||
b.pool.Put(b.buf)
|
||||
b.buf = nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type constBufferPool struct {
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
func (p *constBufferPool) Get(int) *[]byte {
|
||||
return &p.buffer
|
||||
}
|
||||
|
||||
func (p *constBufferPool) Put(*[]byte) {}
|
||||
414
internal/transport/readyreader/ready_reader_ext_test.go
Normal file
414
internal/transport/readyreader/ready_reader_ext_test.go
Normal file
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
*
|
||||
* 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
185
internal/transport/server_metrics_ext_test.go
Normal file
185
internal/transport/server_metrics_ext_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
*
|
||||
* 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/channelz"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
transportinternal "google.golang.org/grpc/internal/transport/internal"
|
||||
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
func overrideTimeNowFunc(t *testing.T, f func() int64) {
|
||||
orig := transportinternal.TimeNowFunc
|
||||
transportinternal.TimeNowFunc = f
|
||||
t.Cleanup(func() { transportinternal.TimeNowFunc = orig })
|
||||
}
|
||||
|
||||
func verifyResultWithDelay(ctx context.Context, f func() (bool, error)) error {
|
||||
var lastErr error
|
||||
for ; ctx.Err() == nil; <-time.After(10 * time.Millisecond) {
|
||||
var ok bool
|
||||
if ok, lastErr = f(); ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Timeout when waiting for channelz to contain expected data. Last seen error: %v", lastErr)
|
||||
}
|
||||
|
||||
func (s) TestChannelz_ServerSocketMetricsLastMessageTimestamps(t *testing.T) {
|
||||
channelz.TurnOn()
|
||||
defer internal.ChannelzTurnOffForTesting()
|
||||
|
||||
// Override timeNowFunc for the duration of the test.
|
||||
var curTime atomic.Int64
|
||||
overrideTimeNowFunc(t, func() int64 {
|
||||
curTime.Add(1)
|
||||
return curTime.Load()
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Start a stub server that echoes back messages sent by the client. This
|
||||
// allows us to test both LastMessageReceivedTimestamp and
|
||||
// LastMessageSentTimestamp on the server side.
|
||||
var blockBeforeSend = make(chan struct{}, 1)
|
||||
ss := &stubserver.StubServer{
|
||||
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-blockBeforeSend:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := stream.Send(&testpb.StreamingOutputCallResponse{Payload: req.GetPayload()}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
if err := ss.Start(nil); err != nil {
|
||||
t.Fatalf("Failed to start test server: %v", err)
|
||||
}
|
||||
defer ss.Stop()
|
||||
|
||||
// Wait for the server to be up and registered with channelz.
|
||||
var svrID int64
|
||||
if err := verifyResultWithDelay(ctx, func() (bool, error) {
|
||||
ssrvs, _ := channelz.GetServers(0, 0)
|
||||
for _, s := range ssrvs {
|
||||
for id := range s.ListenSockets() {
|
||||
skt := channelz.GetSocket(id)
|
||||
if skt != nil && skt.LocalAddr.String() == ss.Address {
|
||||
svrID = s.ID
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("could not find server with address %s in channelz", ss.Address)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stream, err := ss.Client.FullDuplexCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("FullDuplexCall() failed: %v", err)
|
||||
}
|
||||
|
||||
// Wait for the message from the client to be received on the server and
|
||||
// timestamp updated.
|
||||
wantTimestamp := int64(1)
|
||||
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
|
||||
t.Fatalf("stream.Send(_) = %v, want <nil>", err)
|
||||
}
|
||||
if err := verifyResultWithDelay(ctx, func() (bool, error) {
|
||||
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
|
||||
if len(ns) == 0 {
|
||||
return false, fmt.Errorf("no server sockets found")
|
||||
}
|
||||
sktData := &ns[0].SocketMetrics
|
||||
if sktData.MessagesReceived.Load() < 1 {
|
||||
return false, fmt.Errorf("server has not received any messages yet")
|
||||
}
|
||||
if gotTimestamp := sktData.LastMessageReceivedTimestamp.Load(); gotTimestamp != wantTimestamp {
|
||||
return false, fmt.Errorf("LastMessageReceivedTimestamp is %d, want %d", gotTimestamp, wantTimestamp)
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blockBeforeSend <- struct{}{}
|
||||
|
||||
// Wait for the message to be sent by the server and timestamp updated.
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
t.Fatalf("stream.Recv() = %v, want <nil>", err)
|
||||
}
|
||||
wantTimestamp = int64(2)
|
||||
if err := verifyResultWithDelay(ctx, func() (bool, error) {
|
||||
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
|
||||
sktData := &ns[0].SocketMetrics
|
||||
if sktData.MessagesSent.Load() == 0 {
|
||||
return false, fmt.Errorf("server has not sent any messages yet")
|
||||
}
|
||||
if gotTimestamp := sktData.LastMessageSentTimestamp.Load(); gotTimestamp != wantTimestamp {
|
||||
return false, fmt.Errorf("LastMessageSentTimestamp = %d, want = %d", gotTimestamp, wantTimestamp)
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Send another message to verify that LastMessageReceivedTimestamp is
|
||||
// updated again.
|
||||
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
|
||||
t.Fatalf("stream.Send(_) = %v, want <nil>", err)
|
||||
}
|
||||
wantTimestamp = int64(3)
|
||||
if err := verifyResultWithDelay(ctx, func() (bool, error) {
|
||||
ns, _ := channelz.GetServerSockets(svrID, 0, 0)
|
||||
sktData := &ns[0].SocketMetrics
|
||||
if sktData.MessagesReceived.Load() < 2 {
|
||||
return false, fmt.Errorf("server has not received second message yet")
|
||||
}
|
||||
if gotTimestamp := sktData.LastMessageReceivedTimestamp.Load(); gotTimestamp != wantTimestamp {
|
||||
return false, fmt.Errorf("LastMessageReceivedTimestamp is %d, want %d", gotTimestamp, wantTimestamp)
|
||||
}
|
||||
return true, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
189
internal/transport/server_stream.go
Normal file
189
internal/transport/server_stream.go
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2024 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 (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ServerStream implements streaming functionality for a gRPC server.
|
||||
type ServerStream struct {
|
||||
Stream // Embed for common stream functionality.
|
||||
|
||||
st internalServerTransport
|
||||
ctxDone <-chan struct{} // closed at the end of stream. Cache of ctx.Done() (for performance)
|
||||
// cancel is invoked at the end of stream to cancel ctx. It also stops the
|
||||
// timer for monitoring the rpc deadline if configured.
|
||||
cancel func()
|
||||
|
||||
// Holds compressor names passed in grpc-accept-encoding metadata from the
|
||||
// client.
|
||||
clientAdvertisedCompressors string
|
||||
|
||||
// hdrMu protects outgoing header and trailer metadata.
|
||||
hdrMu sync.Mutex
|
||||
header metadata.MD // the outgoing header metadata. Updated by WriteHeader.
|
||||
headerSent atomic.Bool // atomically set when the headers are sent out.
|
||||
|
||||
headerWireLength int
|
||||
}
|
||||
|
||||
// Read reads an n byte message from the input stream.
|
||||
func (s *ServerStream) Read(n int) (mem.BufferSlice, error) {
|
||||
b, err := s.Stream.read(n)
|
||||
if err == nil {
|
||||
s.st.incrMsgRecv()
|
||||
}
|
||||
return b, err
|
||||
}
|
||||
|
||||
// SendHeader sends the header metadata for the given stream.
|
||||
func (s *ServerStream) SendHeader(md metadata.MD) error {
|
||||
return s.st.writeHeader(s, md)
|
||||
}
|
||||
|
||||
// Write writes the hdr and data bytes to the output stream.
|
||||
func (s *ServerStream) Write(hdr []byte, data mem.BufferSlice, opts *WriteOptions) error {
|
||||
return s.st.write(s, hdr, data, opts)
|
||||
}
|
||||
|
||||
// WriteStatus sends the status of a stream to the client. WriteStatus is
|
||||
// the final call made on a stream and always occurs.
|
||||
func (s *ServerStream) WriteStatus(st *status.Status) error {
|
||||
return s.st.writeStatus(s, st)
|
||||
}
|
||||
|
||||
// isHeaderSent indicates whether headers have been sent.
|
||||
func (s *ServerStream) isHeaderSent() bool {
|
||||
return s.headerSent.Load()
|
||||
}
|
||||
|
||||
// updateHeaderSent updates headerSent and returns true
|
||||
// if it was already set.
|
||||
func (s *ServerStream) updateHeaderSent() bool {
|
||||
return s.headerSent.Swap(true)
|
||||
}
|
||||
|
||||
// RecvCompress returns the compression algorithm applied to the inbound
|
||||
// message. It is empty string if there is no compression applied.
|
||||
func (s *ServerStream) RecvCompress() string {
|
||||
return s.recvCompress
|
||||
}
|
||||
|
||||
// SendCompress returns the send compressor name.
|
||||
func (s *ServerStream) SendCompress() string {
|
||||
return s.sendCompress
|
||||
}
|
||||
|
||||
// ContentSubtype returns the content-subtype for a request. For example, a
|
||||
// content-subtype of "proto" will result in a content-type of
|
||||
// "application/grpc+proto". This will always be lowercase. See
|
||||
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
|
||||
// more details.
|
||||
func (s *ServerStream) ContentSubtype() string {
|
||||
return s.contentSubtype
|
||||
}
|
||||
|
||||
// SetSendCompress sets the compression algorithm to the stream.
|
||||
func (s *ServerStream) SetSendCompress(name string) error {
|
||||
if s.isHeaderSent() || s.getState() == streamDone {
|
||||
return errors.New("transport: set send compressor called after headers sent or stream done")
|
||||
}
|
||||
|
||||
s.sendCompress = name
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetContext sets the context of the stream. This will be deleted once the
|
||||
// stats handler callouts all move to gRPC layer.
|
||||
func (s *ServerStream) SetContext(ctx context.Context) {
|
||||
s.ctx = ctx
|
||||
}
|
||||
|
||||
// ClientAdvertisedCompressors returns the compressor names advertised by the
|
||||
// client via grpc-accept-encoding header.
|
||||
func (s *ServerStream) ClientAdvertisedCompressors() []string {
|
||||
values := strings.Split(s.clientAdvertisedCompressors, ",")
|
||||
for i, v := range values {
|
||||
values[i] = strings.TrimSpace(v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Header returns the header metadata of the stream. It returns the out header
|
||||
// after t.WriteHeader is called. It does not block and must not be called
|
||||
// until after WriteHeader.
|
||||
func (s *ServerStream) Header() (metadata.MD, error) {
|
||||
// Return the header in stream. It will be the out
|
||||
// header after t.WriteHeader is called.
|
||||
return s.header.Copy(), nil
|
||||
}
|
||||
|
||||
// HeaderWireLength returns the size of the headers of the stream as received
|
||||
// from the wire.
|
||||
func (s *ServerStream) HeaderWireLength() int {
|
||||
return s.headerWireLength
|
||||
}
|
||||
|
||||
// SetHeader sets the header metadata. This can be called multiple times.
|
||||
// This should not be called in parallel to other data writes.
|
||||
func (s *ServerStream) SetHeader(md metadata.MD) error {
|
||||
if md.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
if s.isHeaderSent() || s.getState() == streamDone {
|
||||
return ErrIllegalHeaderWrite
|
||||
}
|
||||
s.hdrMu.Lock()
|
||||
s.header = metadata.Join(s.header, md)
|
||||
s.hdrMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTrailer sets the trailer metadata which will be sent with the RPC status
|
||||
// by the server. This can be called multiple times.
|
||||
// This should not be called parallel to other data writes.
|
||||
func (s *ServerStream) SetTrailer(md metadata.MD) error {
|
||||
if md.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
if s.getState() == streamDone {
|
||||
return ErrIllegalHeaderWrite
|
||||
}
|
||||
s.hdrMu.Lock()
|
||||
s.trailer = metadata.Join(s.trailer, md)
|
||||
s.hdrMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerStream) requestRead(n int) {
|
||||
s.st.adjustWindow(s, uint32(n))
|
||||
}
|
||||
|
||||
func (s *ServerStream) updateWindow(n int) {
|
||||
s.st.updateWindow(s, uint32(n))
|
||||
}
|
||||
878
internal/transport/transport.go
Normal file
878
internal/transport/transport.go
Normal file
@@ -0,0 +1,878 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2014 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 defines and implements message oriented communication
|
||||
// channel to complete various transactions (e.g., an RPC). It is meant for
|
||||
// grpc-internal usage and is not intended to be imported directly by users.
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal/channelz"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
imem "google.golang.org/grpc/internal/mem"
|
||||
"google.golang.org/grpc/internal/transport/internal"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/mem"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/stats"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/tap"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevel = 2
|
||||
// recvMsgSize estimates the memory overhead of a recvMsg in the backlog.
|
||||
// It accounts for the recvMsg struct itself and the slice header of the
|
||||
// underlying buffer's data.
|
||||
recvMsgSize = int(unsafe.Sizeof(recvMsg{}) + unsafe.Sizeof([]byte{}))
|
||||
|
||||
// utilizationFactor controls when we consider memory utilization acceptable.
|
||||
// When backlogHeapSize / payloadSize <= utilizationFactor (meaning at least
|
||||
// 50% of the heap memory is actual payload data), compaction is skipped.
|
||||
utilizationFactor = 2
|
||||
)
|
||||
|
||||
var (
|
||||
// compactionThreshold is approx 57KB (on 64-bit systems). It allows
|
||||
// accumulating up to 1024 1-byte payloads before triggering compaction.
|
||||
//
|
||||
// Because individual payloads <= 1024 bytes are allocated on the heap
|
||||
// outside mem.BufferPool, waiting for at least 1024 bytes to accumulate
|
||||
// ensures that compaction coalesces those small heap allocations into a
|
||||
// single large buffer from mem.BufferPool, enabling buffer reuse while
|
||||
// avoiding frequent copying for small bursts of frames.
|
||||
compactionThreshold = imem.BufferPoolingThreshold * (recvMsgSize + 1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.TimeNowFunc = func() int64 { return time.Now().UnixNano() }
|
||||
}
|
||||
|
||||
// recvMsg represents the received msg from the transport. All transport
|
||||
// protocol specific info has been removed.
|
||||
type recvMsg struct {
|
||||
buffer mem.Buffer
|
||||
// nil: received some data
|
||||
// io.EOF: stream is completed. data is nil.
|
||||
// other non-nil error: transport failure. data is nil.
|
||||
err error
|
||||
}
|
||||
|
||||
// recvBuffer is an unbounded channel of recvMsg structs.
|
||||
//
|
||||
// Note: recvBuffer differs from buffer.Unbounded in that it provides in-place
|
||||
// value initialization (via init) to avoid struct pointer allocations on
|
||||
// streams, and automatically frees pooled mem.Buffer payloads when stream
|
||||
// errors occur.
|
||||
type recvBuffer struct {
|
||||
c chan recvMsg
|
||||
mu sync.Mutex
|
||||
backlog []recvMsg
|
||||
// uncompactedSuffixLen tracks the number of consecutive data messages at
|
||||
// the tail of backlog that have not been compacted.
|
||||
uncompactedSuffixLen int
|
||||
// uncompactedBytes tracks the total payload bytes across the trailing
|
||||
// uncompactedSuffixLen messages.
|
||||
uncompactedBytes int
|
||||
err error
|
||||
bufPool mem.BufferPool
|
||||
}
|
||||
|
||||
// init allows a recvBuffer to be initialized in-place, which is useful
|
||||
// for resetting a buffer or for avoiding a heap allocation when the buffer
|
||||
// is embedded in another struct.
|
||||
func (b *recvBuffer) init(pool mem.BufferPool) {
|
||||
b.c = make(chan recvMsg, 1)
|
||||
b.bufPool = pool
|
||||
}
|
||||
|
||||
func (b *recvBuffer) put(r recvMsg) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.err != nil {
|
||||
// drop the buffer on the floor. Since b.err is not nil, any subsequent reads
|
||||
// will always return an error, making this buffer inaccessible.
|
||||
r.buffer.Free()
|
||||
// An error had occurred earlier, don't accept more
|
||||
// data or errors.
|
||||
return
|
||||
}
|
||||
b.err = r.err
|
||||
if len(b.backlog) == 0 {
|
||||
select {
|
||||
case b.c <- r:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
b.backlog = append(b.backlog, r)
|
||||
b.compactBacklogLocked(r)
|
||||
}
|
||||
|
||||
func (b *recvBuffer) compactBacklogLocked(r recvMsg) {
|
||||
if !envconfig.EnableReceiveBufferCompaction {
|
||||
return
|
||||
}
|
||||
if r.buffer == nil {
|
||||
b.uncompactedBytes = 0
|
||||
b.uncompactedSuffixLen = 0
|
||||
return
|
||||
}
|
||||
|
||||
b.uncompactedSuffixLen++
|
||||
b.uncompactedBytes += r.buffer.Len()
|
||||
backlogHeapSize := b.uncompactedSuffixLen*recvMsgSize + b.uncompactedBytes
|
||||
|
||||
// If the memory overhead is less than 50% of the heap usage (e.g., because
|
||||
// a large DATA frame arrived), the average message size in the suffix is
|
||||
// large enough that memory bloat is not a concern. Reset suffix tracking.
|
||||
if backlogHeapSize <= utilizationFactor*b.uncompactedBytes {
|
||||
b.uncompactedBytes = 0
|
||||
b.uncompactedSuffixLen = 0
|
||||
return
|
||||
}
|
||||
// Avoid compacting too frequently for short bursts of small frames.
|
||||
// Wait until we have accumulated at least ~1024 small messages (~57 KB).
|
||||
if backlogHeapSize <= compactionThreshold {
|
||||
// Still can accumulate more payloads.
|
||||
return
|
||||
}
|
||||
|
||||
// Since the memory utilization is less than 50%, the average payload size
|
||||
// of each recvMsg must be less than recvMsgSize (approx 56 bytes).
|
||||
// In the worst case for bytes copied (where the average payload is just
|
||||
// below recvMsgSize), compaction will occur once every:
|
||||
// compactionThreshold / (recvMsgSize + avg_payload) = ~520 messages,
|
||||
// copying ~29KB of data.
|
||||
|
||||
start := 0
|
||||
newBuf := b.bufPool.Get(b.uncompactedBytes)
|
||||
startIdx := len(b.backlog) - b.uncompactedSuffixLen
|
||||
|
||||
for i := startIdx; i < len(b.backlog); i++ {
|
||||
m := b.backlog[i]
|
||||
b.backlog[i] = recvMsg{}
|
||||
start += copy((*newBuf)[start:], m.buffer.ReadOnlyData())
|
||||
m.buffer.Free()
|
||||
}
|
||||
b.backlog[startIdx] = recvMsg{
|
||||
buffer: mem.NewBuffer(newBuf, b.bufPool),
|
||||
}
|
||||
b.backlog = b.backlog[:startIdx+1]
|
||||
// After compaction, the suffix is replaced with a single message containing
|
||||
// the combined payload. The new utilization is close to 1.0 (overhead of
|
||||
// one recvMsg relative to the large compacted payload), which is well
|
||||
// below the utilization factor of 2.
|
||||
b.uncompactedBytes = 0
|
||||
b.uncompactedSuffixLen = 0
|
||||
}
|
||||
|
||||
func (b *recvBuffer) load() {
|
||||
b.mu.Lock()
|
||||
if len(b.backlog) > 0 {
|
||||
select {
|
||||
case b.c <- b.backlog[0]:
|
||||
// backlog[0] is only part of the tracked uncompacted suffix if the
|
||||
// entire backlog currently consists of the suffix. If an earlier
|
||||
// compaction or reset occurred, backlog[0] is already compacted.
|
||||
if envconfig.EnableReceiveBufferCompaction && b.uncompactedSuffixLen == len(b.backlog) {
|
||||
b.uncompactedSuffixLen--
|
||||
b.uncompactedBytes -= b.backlog[0].buffer.Len()
|
||||
}
|
||||
b.backlog[0] = recvMsg{}
|
||||
b.backlog = b.backlog[1:]
|
||||
default:
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// get returns the channel that receives a recvMsg in the buffer.
|
||||
//
|
||||
// Upon receipt of a recvMsg, the caller should call load to send another
|
||||
// recvMsg onto the channel if there is any.
|
||||
func (b *recvBuffer) get() <-chan recvMsg {
|
||||
return b.c
|
||||
}
|
||||
|
||||
// recvBufferReader implements io.Reader interface to read the data from
|
||||
// recvBuffer.
|
||||
type recvBufferReader struct {
|
||||
_ noCopy
|
||||
clientStream *ClientStream // The client transport stream is closed with a status representing ctx.Err() and nil trailer metadata.
|
||||
ctx context.Context
|
||||
ctxDone <-chan struct{} // cache of ctx.Done() (for performance).
|
||||
recv *recvBuffer
|
||||
last mem.Buffer // Stores the remaining data in the previous calls.
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) ReadMessageHeader(header []byte) (n int, err error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
if r.last != nil {
|
||||
n, r.last = mem.ReadUnsafe(header, r.last)
|
||||
return n, nil
|
||||
}
|
||||
if r.clientStream != nil {
|
||||
n, r.err = r.readMessageHeaderClient(header)
|
||||
} else {
|
||||
n, r.err = r.readMessageHeader(header)
|
||||
}
|
||||
return n, r.err
|
||||
}
|
||||
|
||||
// Read reads the next n bytes from last. If last is drained, it tries to read
|
||||
// additional data from recv. It blocks if there no additional data available in
|
||||
// recv. If Read returns any non-nil error, it will continue to return that
|
||||
// error.
|
||||
func (r *recvBufferReader) Read(n int) (buf mem.Buffer, err error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.last != nil {
|
||||
buf = r.last
|
||||
if r.last.Len() > n {
|
||||
buf, r.last = mem.SplitUnsafe(buf, n)
|
||||
} else {
|
||||
r.last = nil
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
if r.clientStream != nil {
|
||||
buf, r.err = r.readClient(n)
|
||||
} else {
|
||||
buf, r.err = r.read(n)
|
||||
}
|
||||
return buf, r.err
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) readMessageHeader(header []byte) (n int, err error) {
|
||||
select {
|
||||
case <-r.ctxDone:
|
||||
return 0, ContextErr(r.ctx.Err())
|
||||
case m := <-r.recv.get():
|
||||
return r.readMessageHeaderAdditional(m, header)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) read(n int) (buf mem.Buffer, err error) {
|
||||
select {
|
||||
case <-r.ctxDone:
|
||||
return nil, ContextErr(r.ctx.Err())
|
||||
case m := <-r.recv.get():
|
||||
return r.readAdditional(m, n)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) readMessageHeaderClient(header []byte) (n int, err error) {
|
||||
// If the context is canceled, then closes the stream with nil metadata.
|
||||
// closeStream writes its error parameter to r.recv as a recvMsg.
|
||||
// r.readAdditional acts on that message and returns the necessary error.
|
||||
select {
|
||||
case <-r.ctxDone:
|
||||
// Note that this adds the ctx error to the end of recv buffer, and
|
||||
// reads from the head. This will delay the error until recv buffer is
|
||||
// empty, thus will delay ctx cancellation in Recv().
|
||||
//
|
||||
// It's done this way to fix a race between ctx cancel and trailer. The
|
||||
// race was, stream.Recv() may return ctx error if ctxDone wins the
|
||||
// race, but stream.Trailer() may return a non-nil md because the stream
|
||||
// was not marked as done when trailer is received. This closeStream
|
||||
// call will mark stream as done, thus fix the race.
|
||||
//
|
||||
// TODO: delaying ctx error seems like a unnecessary side effect. What
|
||||
// we really want is to mark the stream as done, and return ctx error
|
||||
// faster.
|
||||
r.clientStream.Close(ContextErr(r.ctx.Err()))
|
||||
m := <-r.recv.get()
|
||||
return r.readMessageHeaderAdditional(m, header)
|
||||
case m := <-r.recv.get():
|
||||
return r.readMessageHeaderAdditional(m, header)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) readClient(n int) (buf mem.Buffer, err error) {
|
||||
// If the context is canceled, then closes the stream with nil metadata.
|
||||
// closeStream writes its error parameter to r.recv as a recvMsg.
|
||||
// r.readAdditional acts on that message and returns the necessary error.
|
||||
select {
|
||||
case <-r.ctxDone:
|
||||
// Note that this adds the ctx error to the end of recv buffer, and
|
||||
// reads from the head. This will delay the error until recv buffer is
|
||||
// empty, thus will delay ctx cancellation in Recv().
|
||||
//
|
||||
// It's done this way to fix a race between ctx cancel and trailer. The
|
||||
// race was, stream.Recv() may return ctx error if ctxDone wins the
|
||||
// race, but stream.Trailer() may return a non-nil md because the stream
|
||||
// was not marked as done when trailer is received. This closeStream
|
||||
// call will mark stream as done, thus fix the race.
|
||||
//
|
||||
// TODO: delaying ctx error seems like a unnecessary side effect. What
|
||||
// we really want is to mark the stream as done, and return ctx error
|
||||
// faster.
|
||||
r.clientStream.Close(ContextErr(r.ctx.Err()))
|
||||
m := <-r.recv.get()
|
||||
return r.readAdditional(m, n)
|
||||
case m := <-r.recv.get():
|
||||
return r.readAdditional(m, n)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) readMessageHeaderAdditional(m recvMsg, header []byte) (n int, err error) {
|
||||
r.recv.load()
|
||||
if m.err != nil {
|
||||
if m.buffer != nil {
|
||||
m.buffer.Free()
|
||||
}
|
||||
return 0, m.err
|
||||
}
|
||||
|
||||
n, r.last = mem.ReadUnsafe(header, m.buffer)
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *recvBufferReader) readAdditional(m recvMsg, n int) (b mem.Buffer, err error) {
|
||||
r.recv.load()
|
||||
if m.err != nil {
|
||||
if m.buffer != nil {
|
||||
m.buffer.Free()
|
||||
}
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
if m.buffer.Len() > n {
|
||||
m.buffer, r.last = mem.SplitUnsafe(m.buffer, n)
|
||||
}
|
||||
|
||||
return m.buffer, nil
|
||||
}
|
||||
|
||||
type streamState uint32
|
||||
|
||||
const (
|
||||
streamActive streamState = iota
|
||||
streamWriteDone // EndStream sent
|
||||
streamReadDone // EndStream received
|
||||
streamDone // the entire stream is finished.
|
||||
)
|
||||
|
||||
// Stream represents an RPC in the transport layer.
|
||||
type Stream struct {
|
||||
ctx context.Context // the associated context of the stream
|
||||
method string // the associated RPC method of the stream
|
||||
recvCompress string
|
||||
sendCompress string
|
||||
|
||||
readRequester readRequester
|
||||
|
||||
// contentSubtype is the content-subtype for requests.
|
||||
// this must be lowercase or the behavior is undefined.
|
||||
contentSubtype string
|
||||
|
||||
trailer metadata.MD // the key-value map of trailer metadata.
|
||||
|
||||
// Non-pointer fields are at the end to optimize GC performance.
|
||||
state streamState
|
||||
id uint32
|
||||
buf recvBuffer
|
||||
trReader transportReader
|
||||
fc inFlow
|
||||
wq writeQuota
|
||||
}
|
||||
|
||||
// readRequester is used to state application's intentions to read data. This
|
||||
// is used to adjust flow control, if needed.
|
||||
type readRequester interface {
|
||||
requestRead(int)
|
||||
}
|
||||
|
||||
func (s *Stream) swapState(st streamState) streamState {
|
||||
return streamState(atomic.SwapUint32((*uint32)(&s.state), uint32(st)))
|
||||
}
|
||||
|
||||
func (s *Stream) compareAndSwapState(oldState, newState streamState) bool {
|
||||
return atomic.CompareAndSwapUint32((*uint32)(&s.state), uint32(oldState), uint32(newState))
|
||||
}
|
||||
|
||||
func (s *Stream) getState() streamState {
|
||||
return streamState(atomic.LoadUint32((*uint32)(&s.state)))
|
||||
}
|
||||
|
||||
// Trailer returns the cached trailer metadata. Note that if it is not called
|
||||
// after the entire stream is done, it could return an empty MD.
|
||||
// It can be safely read only after stream has ended that is either read
|
||||
// or write have returned io.EOF.
|
||||
func (s *Stream) Trailer() metadata.MD {
|
||||
return s.trailer.Copy()
|
||||
}
|
||||
|
||||
// Context returns the context of the stream.
|
||||
func (s *Stream) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
// Method returns the method for the stream.
|
||||
func (s *Stream) Method() string {
|
||||
return s.method
|
||||
}
|
||||
|
||||
func (s *Stream) write(m recvMsg) {
|
||||
s.buf.put(m)
|
||||
}
|
||||
|
||||
// ReadMessageHeader reads data into the provided header slice from the stream.
|
||||
// It first checks if there was an error during a previous read operation and
|
||||
// returns it if present. It then requests a read operation for the length of
|
||||
// the header. It continues to read from the stream until the entire header
|
||||
// slice is filled or an error occurs. If an `io.EOF` error is encountered with
|
||||
// partially read data, it is converted to `io.ErrUnexpectedEOF` to indicate an
|
||||
// unexpected end of the stream. The method returns any error encountered during
|
||||
// the read process or nil if the header was successfully read.
|
||||
func (s *Stream) ReadMessageHeader(header []byte) (err error) {
|
||||
// Don't request a read if there was an error earlier
|
||||
if er := s.trReader.er; er != nil {
|
||||
return er
|
||||
}
|
||||
s.readRequester.requestRead(len(header))
|
||||
bytesRead := 0
|
||||
for len(header) != 0 {
|
||||
n, err := s.trReader.ReadMessageHeader(header)
|
||||
bytesRead += n
|
||||
header = header[n:]
|
||||
if len(header) == 0 {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
if bytesRead > 0 && err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
s.trReader.er = err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ceil returns the ceil after dividing the numerator and denominator while
|
||||
// avoiding integer overflows.
|
||||
func ceil(numerator, denominator int) int {
|
||||
if numerator == 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator-1)/denominator + 1
|
||||
}
|
||||
|
||||
// Read reads n bytes from the wire for this stream.
|
||||
func (s *Stream) read(n int) (data mem.BufferSlice, err error) {
|
||||
// Don't request a read if there was an error earlier
|
||||
if er := s.trReader.er; er != nil {
|
||||
return nil, er
|
||||
}
|
||||
// gRPC Go accepts data frames with a maximum length of 16KB. Larger
|
||||
// messages must be split into multiple frames. We pre-allocate the
|
||||
// buffer to avoid resizing during the read loop, but cap the initial
|
||||
// capacity to 128 frames (2MB) to prevent over-allocation or panics
|
||||
// when reading extremely large streams.
|
||||
allocCap := min(ceil(n, http2MaxFrameLen), 128)
|
||||
data = make(mem.BufferSlice, 0, allocCap)
|
||||
s.readRequester.requestRead(n)
|
||||
bytesRead := 0
|
||||
for n != 0 {
|
||||
buf, err := s.trReader.Read(n)
|
||||
var bufLen int
|
||||
if buf != nil {
|
||||
bufLen = buf.Len()
|
||||
}
|
||||
bytesRead += bufLen
|
||||
n -= bufLen
|
||||
if n == 0 {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
if bytesRead > 0 && err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
s.trReader.er = err
|
||||
}
|
||||
data.Free()
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, buf)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// noCopy may be embedded into structs which must not be copied
|
||||
// after the first use.
|
||||
//
|
||||
// See https://golang.org/issues/8005#issuecomment-190753527
|
||||
// for details.
|
||||
type noCopy struct {
|
||||
}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
func (*noCopy) Unlock() {}
|
||||
|
||||
// transportReader reads all the data available for this Stream from the transport and
|
||||
// passes them into the decoder, which converts them into a gRPC message stream.
|
||||
// The error is io.EOF when the stream is done or another non-nil error if
|
||||
// the stream broke.
|
||||
type transportReader struct {
|
||||
_ noCopy
|
||||
// The handler to control the window update procedure for both this
|
||||
// particular stream and the associated transport.
|
||||
windowHandler windowHandler
|
||||
er error
|
||||
reader recvBufferReader
|
||||
}
|
||||
|
||||
// The handler to control the window update procedure for both this
|
||||
// particular stream and the associated transport.
|
||||
type windowHandler interface {
|
||||
updateWindow(int)
|
||||
}
|
||||
|
||||
func (t *transportReader) ReadMessageHeader(header []byte) (int, error) {
|
||||
n, err := t.reader.ReadMessageHeader(header)
|
||||
if err != nil {
|
||||
t.er = err
|
||||
return 0, err
|
||||
}
|
||||
t.windowHandler.updateWindow(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (t *transportReader) Read(n int) (mem.Buffer, error) {
|
||||
buf, err := t.reader.Read(n)
|
||||
if err != nil {
|
||||
t.er = err
|
||||
return buf, err
|
||||
}
|
||||
t.windowHandler.updateWindow(buf.Len())
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// GoString is implemented by Stream so context.String() won't
|
||||
// race when printing %#v.
|
||||
func (s *Stream) GoString() string {
|
||||
return fmt.Sprintf("<stream: %p, %v>", s, s.method)
|
||||
}
|
||||
|
||||
// state of transport
|
||||
type transportState int
|
||||
|
||||
const (
|
||||
reachable transportState = iota
|
||||
closing
|
||||
draining
|
||||
)
|
||||
|
||||
// ServerConfig consists of all the configurations to establish a server transport.
|
||||
type ServerConfig struct {
|
||||
MaxStreams uint32
|
||||
ConnectionTimeout time.Duration
|
||||
Credentials credentials.TransportCredentials
|
||||
InTapHandle tap.ServerInHandle
|
||||
StatsHandler stats.Handler
|
||||
KeepaliveParams keepalive.ServerParameters
|
||||
KeepalivePolicy keepalive.EnforcementPolicy
|
||||
InitialWindowSize int32
|
||||
InitialConnWindowSize int32
|
||||
WriteBufferSize int
|
||||
ReadBufferSize int
|
||||
SharedWriteBuffer bool
|
||||
ChannelzParent *channelz.Server
|
||||
MaxHeaderListSize *uint32
|
||||
HeaderTableSize *uint32
|
||||
BufferPool mem.BufferPool
|
||||
StaticWindowSize bool
|
||||
}
|
||||
|
||||
// ConnectOptions covers all relevant options for communicating with the server.
|
||||
type ConnectOptions struct {
|
||||
// UserAgent is the application user agent.
|
||||
UserAgent string
|
||||
// Dialer specifies how to dial a network address.
|
||||
Dialer func(context.Context, string) (net.Conn, error)
|
||||
// FailOnNonTempDialError specifies if gRPC fails on non-temporary dial errors.
|
||||
FailOnNonTempDialError bool
|
||||
// PerRPCCredentials stores the PerRPCCredentials required to issue RPCs.
|
||||
PerRPCCredentials []credentials.PerRPCCredentials
|
||||
// TransportCredentials stores the Authenticator required to setup a client
|
||||
// connection. Only one of TransportCredentials and CredsBundle is non-nil.
|
||||
TransportCredentials credentials.TransportCredentials
|
||||
// CredsBundle is the credentials bundle to be used. Only one of
|
||||
// TransportCredentials and CredsBundle is non-nil.
|
||||
CredsBundle credentials.Bundle
|
||||
// KeepaliveParams stores the keepalive parameters.
|
||||
KeepaliveParams keepalive.ClientParameters
|
||||
// StatsHandlers stores the handler for stats.
|
||||
StatsHandlers []stats.Handler
|
||||
// InitialWindowSize sets the initial window size for a stream.
|
||||
InitialWindowSize int32
|
||||
// InitialConnWindowSize sets the initial window size for a connection.
|
||||
InitialConnWindowSize int32
|
||||
// WriteBufferSize sets the size of write buffer which in turn determines how much data can be batched before it's written on the wire.
|
||||
WriteBufferSize int
|
||||
// ReadBufferSize sets the size of read buffer, which in turn determines how much data can be read at most for one read syscall.
|
||||
ReadBufferSize int
|
||||
// SharedWriteBuffer indicates whether connections should reuse write buffer
|
||||
SharedWriteBuffer bool
|
||||
// ChannelzParent sets the addrConn id which initiated the creation of this client transport.
|
||||
ChannelzParent *channelz.SubChannel
|
||||
// MaxHeaderListSize sets the max (uncompressed) size of header list that is prepared to be received.
|
||||
MaxHeaderListSize *uint32
|
||||
// The mem.BufferPool to use when reading/writing to the wire.
|
||||
BufferPool mem.BufferPool
|
||||
// StaticWindowSize controls whether dynamic window sizing is enabled.
|
||||
StaticWindowSize bool
|
||||
}
|
||||
|
||||
// WriteOptions provides additional hints and information for message
|
||||
// transmission.
|
||||
type WriteOptions struct {
|
||||
// Last indicates whether this write is the last piece for
|
||||
// this stream.
|
||||
Last bool
|
||||
}
|
||||
|
||||
// CallHdr carries the information of a particular RPC.
|
||||
type CallHdr struct {
|
||||
// Host specifies the peer's host.
|
||||
Host string
|
||||
|
||||
// Method specifies the operation to perform.
|
||||
Method string
|
||||
|
||||
// SendCompress specifies the compression algorithm applied on
|
||||
// outbound message.
|
||||
SendCompress string
|
||||
|
||||
// AcceptedCompressors overrides the grpc-accept-encoding header for this
|
||||
// call. When nil, the transport advertises the default set of registered
|
||||
// compressors. A non-nil pointer overrides that value (including the empty
|
||||
// string to advertise none).
|
||||
AcceptedCompressors *string
|
||||
|
||||
// Creds specifies credentials.PerRPCCredentials for a call.
|
||||
Creds credentials.PerRPCCredentials
|
||||
|
||||
// ContentSubtype specifies the content-subtype for a request. For example, a
|
||||
// content-subtype of "proto" will result in a content-type of
|
||||
// "application/grpc+proto". The value of ContentSubtype must be all
|
||||
// lowercase, otherwise the behavior is undefined. See
|
||||
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
|
||||
// for more details.
|
||||
ContentSubtype string
|
||||
|
||||
PreviousAttempts int // value of grpc-previous-rpc-attempts header to set
|
||||
|
||||
// Authority is used to explicitly override the `:authority` header.
|
||||
//
|
||||
// This value comes from one of two sources:
|
||||
// 1. The `CallAuthority` call option, if specified by the user.
|
||||
// 2. An override provided by the LB picker (e.g. xDS authority rewriting).
|
||||
//
|
||||
// The `CallAuthority` call option always takes precedence over the LB
|
||||
// picker override.
|
||||
Authority string
|
||||
}
|
||||
|
||||
// ClientTransport is the common interface for all gRPC client-side transport
|
||||
// implementations.
|
||||
type ClientTransport interface {
|
||||
// Close tears down this transport. Once it returns, the transport
|
||||
// should not be accessed any more. The caller must make sure this
|
||||
// is called only once.
|
||||
Close(err error)
|
||||
|
||||
// GracefulClose starts to tear down the transport: the transport will stop
|
||||
// accepting new RPCs and NewStream will return error. Once all streams are
|
||||
// finished, the transport will close.
|
||||
//
|
||||
// It does not block.
|
||||
GracefulClose()
|
||||
|
||||
// NewStream creates a Stream for an RPC.
|
||||
NewStream(ctx context.Context, callHdr *CallHdr, handler stats.Handler) (*ClientStream, error)
|
||||
|
||||
// Error returns a channel that is closed when some I/O error
|
||||
// happens. Typically the caller should have a goroutine to monitor
|
||||
// this in order to take action (e.g., close the current transport
|
||||
// and create a new one) in error case. It should not return nil
|
||||
// once the transport is initiated.
|
||||
Error() <-chan struct{}
|
||||
|
||||
// GoAway returns a channel that is closed when ClientTransport
|
||||
// receives the draining signal from the server (e.g., GOAWAY frame in
|
||||
// HTTP/2).
|
||||
GoAway() <-chan struct{}
|
||||
|
||||
// GetGoAwayReason returns the reason why GoAway frame was received, along
|
||||
// with a human readable string with debug info.
|
||||
GetGoAwayReason() (GoAwayReason, string)
|
||||
|
||||
// Peer returns information about the peer associated with the Transport.
|
||||
// The returned information includes authentication and network address details.
|
||||
Peer() *peer.Peer
|
||||
}
|
||||
|
||||
// ServerTransport is the common interface for all gRPC server-side transport
|
||||
// implementations.
|
||||
//
|
||||
// Methods may be called concurrently from multiple goroutines, but
|
||||
// Write methods for a given Stream will be called serially.
|
||||
type ServerTransport interface {
|
||||
// HandleStreams receives incoming streams using the given handler.
|
||||
HandleStreams(context.Context, func(*ServerStream))
|
||||
|
||||
// Close tears down the transport. Once it is called, the transport
|
||||
// should not be accessed any more. All the pending streams and their
|
||||
// handlers will be terminated asynchronously.
|
||||
Close(err error)
|
||||
|
||||
// Peer returns the peer of the server transport.
|
||||
Peer() *peer.Peer
|
||||
|
||||
// Drain notifies the client this ServerTransport stops accepting new RPCs.
|
||||
Drain(debugData string)
|
||||
}
|
||||
|
||||
type internalServerTransport interface {
|
||||
ServerTransport
|
||||
writeHeader(s *ServerStream, md metadata.MD) error
|
||||
write(s *ServerStream, hdr []byte, data mem.BufferSlice, opts *WriteOptions) error
|
||||
writeStatus(s *ServerStream, st *status.Status) error
|
||||
incrMsgRecv()
|
||||
adjustWindow(s *ServerStream, n uint32)
|
||||
updateWindow(s *ServerStream, n uint32)
|
||||
}
|
||||
|
||||
// connectionErrorf creates an ConnectionError with the specified error description.
|
||||
func connectionErrorf(temp bool, e error, format string, a ...any) ConnectionError {
|
||||
return ConnectionError{
|
||||
Desc: fmt.Sprintf(format, a...),
|
||||
temp: temp,
|
||||
err: e,
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectionError is an error that results in the termination of the
|
||||
// entire connection and the retry of all the active streams.
|
||||
type ConnectionError struct {
|
||||
Desc string
|
||||
temp bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (e ConnectionError) Error() string {
|
||||
return fmt.Sprintf("connection error: desc = %q", e.Desc)
|
||||
}
|
||||
|
||||
// Temporary indicates if this connection error is temporary or fatal.
|
||||
func (e ConnectionError) Temporary() bool {
|
||||
return e.temp
|
||||
}
|
||||
|
||||
// Origin returns the original error of this connection error.
|
||||
func (e ConnectionError) Origin() error {
|
||||
// Never return nil error here.
|
||||
// If the original error is nil, return itself.
|
||||
if e.err == nil {
|
||||
return e
|
||||
}
|
||||
return e.err
|
||||
}
|
||||
|
||||
// Unwrap returns the original error of this connection error or nil when the
|
||||
// origin is nil.
|
||||
func (e ConnectionError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrConnClosing indicates that the transport is closing.
|
||||
ErrConnClosing = connectionErrorf(true, nil, "transport is closing")
|
||||
// errStreamDrain indicates that the stream is rejected because the
|
||||
// connection is draining. This could be caused by goaway or balancer
|
||||
// removing the address.
|
||||
errStreamDrain = status.Error(codes.Unavailable, "the connection is draining")
|
||||
// errStreamDone is returned from write at the client side to indicate application
|
||||
// layer of an error.
|
||||
errStreamDone = errors.New("the stream is done")
|
||||
// StatusGoAway indicates that the server sent a GOAWAY that included this
|
||||
// stream's ID in unprocessed RPCs.
|
||||
statusGoAway = status.New(codes.Unavailable, "the stream is rejected because server is draining the connection")
|
||||
)
|
||||
|
||||
// GoAwayReason contains the reason for the GoAway frame received.
|
||||
type GoAwayReason uint8
|
||||
|
||||
const (
|
||||
// GoAwayInvalid indicates that no GoAway frame is received.
|
||||
GoAwayInvalid GoAwayReason = 0
|
||||
// GoAwayNoReason is the default value when GoAway frame is received.
|
||||
GoAwayNoReason GoAwayReason = 1
|
||||
// GoAwayTooManyPings indicates that a GoAway frame with
|
||||
// ErrCodeEnhanceYourCalm was received and that the debug data said
|
||||
// "too_many_pings".
|
||||
GoAwayTooManyPings GoAwayReason = 2
|
||||
)
|
||||
|
||||
// GoAwayInfo contains metadata about why a connection was closed.
|
||||
type GoAwayInfo struct {
|
||||
// Reason is the parsed reason for an HTTP/2 GOAWAY frame.
|
||||
Reason GoAwayReason
|
||||
// GoAwayCode is the raw HTTP/2 error code received in a GOAWAY frame.
|
||||
GoAwayCode http2.ErrCode
|
||||
// Err is the underlying error that caused the connection to close. It is
|
||||
// populated if the connection was closed due to a socket error or context
|
||||
// cancellation without receiving a GOAWAY frame. If the connection was
|
||||
// closed due to a GOAWAY frame, this field will be nil.
|
||||
Err error
|
||||
}
|
||||
|
||||
// OnCloseFunc is a callback invoked when a ClientTransport closes.
|
||||
type OnCloseFunc func(GoAwayInfo)
|
||||
|
||||
// ContextErr converts the error from context package into a status error.
|
||||
func ContextErr(err error) error {
|
||||
switch err {
|
||||
case context.DeadlineExceeded:
|
||||
return status.Error(codes.DeadlineExceeded, err.Error())
|
||||
case context.Canceled:
|
||||
return status.Error(codes.Canceled, err.Error())
|
||||
}
|
||||
return status.Errorf(codes.Internal, "Unexpected error from context packet: %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user