@@ -39,9 +39,11 @@ import (
3939 "github.com/google/go-cmp/cmp"
4040 "golang.org/x/net/http2"
4141 "golang.org/x/net/http2/hpack"
42+
4243 "google.golang.org/grpc/attributes"
4344 "google.golang.org/grpc/codes"
4445 "google.golang.org/grpc/credentials"
46+ "google.golang.org/grpc/internal"
4547 "google.golang.org/grpc/internal/channelz"
4648 "google.golang.org/grpc/internal/grpctest"
4749 "google.golang.org/grpc/internal/leakcheck"
@@ -3260,3 +3262,125 @@ func (s) TestClientTransport_Handle1xxHeaders(t *testing.T) {
32603262 })
32613263 }
32623264}
3265+
3266+ func (s ) TestDeleteStreamMetricsIncrementedOnlyOnce (t * testing.T ) {
3267+ // Enable channelz for metrics collection
3268+ defer internal .ChannelzTurnOffForTesting ()
3269+ if ! channelz .IsOn () {
3270+ channelz .TurnOn ()
3271+ }
3272+
3273+ for _ , test := range []struct {
3274+ name string
3275+ eosReceived bool
3276+ wantStreamSucceeded int64
3277+ wantStreamFailed int64
3278+ }{
3279+ {
3280+ name : "StreamsSucceeded" ,
3281+ eosReceived : true ,
3282+ wantStreamSucceeded : 1 ,
3283+ wantStreamFailed : 0 ,
3284+ },
3285+ {
3286+ name : "StreamsFailed" ,
3287+ eosReceived : false ,
3288+ wantStreamSucceeded : 0 ,
3289+ wantStreamFailed : 1 ,
3290+ },
3291+ } {
3292+ t .Run (test .name , func (t * testing.T ) {
3293+ ctx , cancel := context .WithTimeout (context .Background (), defaultTestTimeout )
3294+ defer cancel ()
3295+
3296+ // Setup server configuration with channelz support
3297+ serverConfig := & ServerConfig {
3298+ ChannelzParent : channelz .RegisterServer (t .Name ()),
3299+ }
3300+ defer channelz .RemoveEntry (serverConfig .ChannelzParent .ID )
3301+
3302+ // Create server and client with normal handler (not notifyCall)
3303+ server , client , cancel := setUpWithOptions (t , 0 , serverConfig , normal , ConnectOptions {})
3304+ defer func () {
3305+ client .Close (fmt .Errorf ("test cleanup" ))
3306+ server .stop ()
3307+ cancel ()
3308+ }()
3309+
3310+ // Wait for connection to be established
3311+ waitWhileTrue (t , func () (bool , error ) {
3312+ server .mu .Lock ()
3313+ defer server .mu .Unlock ()
3314+ if len (server .conns ) == 0 {
3315+ return true , fmt .Errorf ("timed-out while waiting for connection" )
3316+ }
3317+ return false , nil
3318+ })
3319+
3320+ // Get the server transport
3321+ server .mu .Lock ()
3322+ var serverTransport * http2Server
3323+ for st := range server .conns {
3324+ serverTransport = st .(* http2Server )
3325+ break
3326+ }
3327+ server .mu .Unlock ()
3328+
3329+ if serverTransport == nil {
3330+ t .Fatal ("Server transport not found" )
3331+ }
3332+
3333+ clientStream , err := client .NewStream (ctx , & CallHdr {})
3334+ if err != nil {
3335+ t .Fatalf ("Failed to create stream: %v" , err )
3336+ }
3337+
3338+ // Wait for the stream to be created on the server side
3339+ var serverStream * ServerStream
3340+ waitWhileTrue (t , func () (bool , error ) {
3341+ serverTransport .mu .Lock ()
3342+ defer serverTransport .mu .Unlock ()
3343+ for _ , v := range serverTransport .activeStreams {
3344+ if v .id == clientStream .id {
3345+ serverStream = v
3346+ return false , nil
3347+ }
3348+ }
3349+ return true , nil
3350+ })
3351+
3352+ if serverStream == nil {
3353+ t .Fatalf ("Server stream not found for client stream ID %d" , clientStream .id )
3354+ }
3355+
3356+ // First call to deleteStream should remove the stream from activeStreams and update metrics
3357+ serverTransport .deleteStream (serverStream , test .eosReceived )
3358+
3359+ // Check metrics after first deleteStream call
3360+ streamsSucceeded := serverTransport .channelz .SocketMetrics .StreamsSucceeded .Load ()
3361+ streamsFailed := serverTransport .channelz .SocketMetrics .StreamsFailed .Load ()
3362+
3363+ if streamsSucceeded != test .wantStreamSucceeded {
3364+ t .Errorf ("After first deleteStream - StreamsSucceeded: got %d, want %d" , streamsSucceeded , test .wantStreamSucceeded )
3365+ }
3366+ if streamsFailed != test .wantStreamFailed {
3367+ t .Errorf ("After first deleteStream - StreamsFailed: got %d, want %d" , streamsFailed , test .wantStreamFailed )
3368+ }
3369+
3370+ // Additional calls to deleteStream should not change metrics (stream already deleted)
3371+ serverTransport .deleteStream (serverStream , test .eosReceived )
3372+ serverTransport .deleteStream (serverStream , test .eosReceived )
3373+
3374+ // Verify metrics haven't changed after subsequent calls
3375+ additionalStreamsSucceeded := serverTransport .channelz .SocketMetrics .StreamsSucceeded .Load ()
3376+ additionalStreamsFailed := serverTransport .channelz .SocketMetrics .StreamsFailed .Load ()
3377+
3378+ if additionalStreamsSucceeded != test .wantStreamSucceeded {
3379+ t .Errorf ("After multiple deleteStream calls - StreamsSucceeded changed: got %d, want %d" , additionalStreamsSucceeded , test .wantStreamSucceeded )
3380+ }
3381+ if additionalStreamsFailed != test .wantStreamFailed {
3382+ t .Errorf ("After multiple deleteStream calls - StreamsFailed changed: got %d, want %d" , additionalStreamsFailed , test .wantStreamFailed )
3383+ }
3384+ })
3385+ }
3386+ }
0 commit comments