2929import static org .junit .Assert .assertSame ;
3030import static org .junit .Assert .assertThrows ;
3131import static org .junit .Assert .assertTrue ;
32+ import static org .mockito .AdditionalAnswers .delegatesTo ;
3233import static org .mockito .ArgumentMatchers .any ;
34+ import static org .mockito .ArgumentMatchers .argThat ;
3335import static org .mockito .ArgumentMatchers .eq ;
3436import static org .mockito .ArgumentMatchers .isA ;
3537import static org .mockito .ArgumentMatchers .same ;
38+ import static org .mockito .Mockito .inOrder ;
3639import static org .mockito .Mockito .mock ;
3740import static org .mockito .Mockito .never ;
3841import static org .mockito .Mockito .times ;
4851import io .grpc .InternalLogId ;
4952import io .grpc .InternalWithLogId ;
5053import io .grpc .LoadBalancer ;
54+ import io .grpc .MetricInstrument ;
5155import io .grpc .MetricRecorder ;
56+ import io .grpc .NameResolver ;
57+ import io .grpc .SecurityLevel ;
5258import io .grpc .Status ;
5359import io .grpc .SynchronizationContext ;
5460import io .grpc .internal .InternalSubchannel .CallTracingTransport ;
6975import org .junit .Test ;
7076import org .junit .runner .RunWith ;
7177import org .junit .runners .JUnit4 ;
78+ import org .mockito .InOrder ;
7279import org .mockito .Mock ;
7380import org .mockito .junit .MockitoJUnit ;
7481import org .mockito .junit .MockitoRule ;
@@ -82,6 +89,9 @@ public class InternalSubchannelTest {
8289 public final MockitoRule mocks = MockitoJUnit .rule ();
8390
8491 private static final String AUTHORITY = "fakeauthority" ;
92+ private static final String BACKEND_SERVICE = "ice-cream-factory-service" ;
93+ private static final String LOCALITY = "mars-olympus-mons-datacenter" ;
94+ private static final SecurityLevel SECURITY_LEVEL = SecurityLevel .PRIVACY_AND_INTEGRITY ;
8595 private static final String USER_AGENT = "mosaic" ;
8696 private static final ConnectivityStateInfo UNAVAILABLE_STATE =
8797 ConnectivityStateInfo .forTransientFailure (Status .UNAVAILABLE );
@@ -109,6 +119,12 @@ public void uncaughtException(Thread t, Throwable e) {
109119 @ Mock private BackoffPolicy .Provider mockBackoffPolicyProvider ;
110120 @ Mock private ClientTransportFactory mockTransportFactory ;
111121
122+ @ Mock private BackoffPolicy mockBackoffPolicy ;
123+ private MetricRecorder mockMetricRecorder = mock (MetricRecorder .class ,
124+ delegatesTo (new MetricRecorderImpl ()));
125+
126+ private static final long RECONNECT_BACKOFF_DELAY_NANOS = TimeUnit .SECONDS .toNanos (1 );
127+
112128 private final LinkedList <String > callbackInvokes = new LinkedList <>();
113129 private final InternalSubchannel .Callback mockInternalSubchannelCallback =
114130 new InternalSubchannel .Callback () {
@@ -1449,8 +1465,90 @@ private void createInternalSubchannel(boolean reconnectDisabled,
14491465 new ChannelLoggerImpl (subchannelTracer , fakeClock .getTimeProvider ()),
14501466 Collections .emptyList (),
14511467 "" ,
1452- new MetricRecorder () {}
1468+ new MetricRecorder () {
1469+ }
1470+ );
1471+ }
1472+
1473+ @ Test
1474+ public void subchannelStateChanges_triggersMetrics_disconnectionOnly () {
1475+ // 1. Mock the backoff policy
1476+ when (mockBackoffPolicyProvider .get ()).thenReturn (mockBackoffPolicy );
1477+ when (mockBackoffPolicy .nextBackoffNanos ()).thenReturn (RECONNECT_BACKOFF_DELAY_NANOS );
1478+
1479+ // 2. Setup Subchannel with attributes
1480+ SocketAddress addr = mock (SocketAddress .class );
1481+ Attributes eagAttributes = Attributes .newBuilder ()
1482+ .set (NameResolver .ATTR_BACKEND_SERVICE , BACKEND_SERVICE )
1483+ .set (LoadBalancer .ATTR_LOCALITY_NAME , LOCALITY )
1484+ .set (GrpcAttributes .ATTR_SECURITY_LEVEL , SECURITY_LEVEL )
1485+ .build ();
1486+ List <EquivalentAddressGroup > addressGroups =
1487+ Arrays .asList (new EquivalentAddressGroup (Arrays .asList (addr ), eagAttributes ));
1488+ createInternalSubchannel (new EquivalentAddressGroup (addr ));
1489+ InternalLogId logId = InternalLogId .allocate ("Subchannel" , /*details=*/ AUTHORITY );
1490+ ChannelTracer subchannelTracer = new ChannelTracer (logId , 10 ,
1491+ fakeClock .getTimeProvider ().currentTimeNanos (), "Subchannel" );
1492+ LoadBalancer .CreateSubchannelArgs createSubchannelArgs =
1493+ LoadBalancer .CreateSubchannelArgs .newBuilder ().setAddresses (addressGroups ).build ();
1494+ internalSubchannel = new InternalSubchannel (
1495+ createSubchannelArgs , AUTHORITY , USER_AGENT , mockBackoffPolicyProvider ,
1496+ mockTransportFactory , fakeClock .getScheduledExecutorService (),
1497+ fakeClock .getStopwatchSupplier (), syncContext , mockInternalSubchannelCallback , channelz ,
1498+ CallTracer .getDefaultFactory ().create (), subchannelTracer , logId ,
1499+ new ChannelLoggerImpl (subchannelTracer , fakeClock .getTimeProvider ()),
1500+ Collections .emptyList (), AUTHORITY , mockMetricRecorder
1501+ );
1502+
1503+ // --- Action ---
1504+ internalSubchannel .obtainActiveTransport ();
1505+ MockClientTransportInfo transportInfo = transports .poll ();
1506+ assertNotNull (transportInfo );
1507+ transportInfo .listener .transportReady ();
1508+ fakeClock .runDueTasks ();
1509+
1510+ transportInfo .listener .transportShutdown (Status .UNAVAILABLE );
1511+ fakeClock .runDueTasks ();
1512+
1513+ // --- Verification ---
1514+ InOrder inOrder = inOrder (mockMetricRecorder );
1515+
1516+ // Verify successful connection metrics
1517+ inOrder .verify (mockMetricRecorder ).addLongCounter (
1518+ eqMetricInstrumentName ("grpc.subchannel.connection_attempts_succeeded" ),
1519+ eq (1L ),
1520+ eq (Arrays .asList (AUTHORITY )),
1521+ eq (Arrays .asList (BACKEND_SERVICE , LOCALITY ))
1522+ );
1523+ inOrder .verify (mockMetricRecorder ).addLongUpDownCounter (
1524+ eqMetricInstrumentName ("grpc.subchannel.open_connections" ),
1525+ eq (1L ),
1526+ eq (Arrays .asList (AUTHORITY )),
1527+ eq (Arrays .asList ("privacy_and_integrity" , BACKEND_SERVICE , LOCALITY ))
1528+ );
1529+
1530+ inOrder .verify (mockMetricRecorder ).addLongCounter (
1531+ eqMetricInstrumentName ("grpc.subchannel.connection_attempts_failed" ),
1532+ eq (1L ),
1533+ eq (Arrays .asList (AUTHORITY )),
1534+ eq (Arrays .asList (BACKEND_SERVICE , LOCALITY ))
14531535 );
1536+
1537+ // Verify disconnection and automatic failure metrics
1538+ inOrder .verify (mockMetricRecorder ).addLongCounter (
1539+ eqMetricInstrumentName ("grpc.subchannel.disconnections" ),
1540+ eq (1L ),
1541+ eq (Arrays .asList (AUTHORITY )),
1542+ eq (Arrays .asList (BACKEND_SERVICE , LOCALITY , "Peer Pressure" ))
1543+ );
1544+ inOrder .verify (mockMetricRecorder ).addLongUpDownCounter (
1545+ eqMetricInstrumentName ("grpc.subchannel.open_connections" ),
1546+ eq (-1L ),
1547+ eq (Arrays .asList (AUTHORITY )),
1548+ eq (Arrays .asList ("privacy_and_integrity" , BACKEND_SERVICE , LOCALITY ))
1549+ );
1550+
1551+ inOrder .verifyNoMoreInteractions ();
14541552 }
14551553
14561554 private void assertNoCallbackInvoke () {
@@ -1463,5 +1561,13 @@ private void assertExactCallbackInvokes(String ... expectedInvokes) {
14631561 callbackInvokes .clear ();
14641562 }
14651563
1564+ static class MetricRecorderImpl implements MetricRecorder {
1565+ }
1566+
1567+ @ SuppressWarnings ("TypeParameterUnusedInFormals" )
1568+ private <T extends MetricInstrument > T eqMetricInstrumentName (String name ) {
1569+ return argThat (instrument -> instrument .getName ().equals (name ));
1570+ }
1571+
14661572 private static class FakeSocketAddress extends SocketAddress {}
14671573}
0 commit comments