@@ -604,6 +604,58 @@ public void successRateOneOutlier_deadlineExceededIgnored() {
604604 assertEjectedSubchannels (ImmutableSet .of ());
605605 }
606606
607+ /**
608+ * Server-initiated CANCELLED status over the wire (stopDelivery = false) counts as failure
609+ * and results in ejection under success rate algorithm.
610+ */
611+ @ Test
612+ public void successRateOneOutlier_serverInitiatedCancelledEjected () {
613+ OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig .Builder ()
614+ .setMaxEjectionPercent (50 )
615+ .setSuccessRateEjection (
616+ new SuccessRateEjection .Builder ()
617+ .setMinimumHosts (3 )
618+ .setRequestVolume (10 ).build ())
619+ .setChildConfig (newChildConfig (roundRobinLbProvider , null )).build ();
620+
621+ loadBalancer .acceptResolvedAddresses (buildResolvedAddress (config , servers ));
622+
623+ // subchannel1 returns CANCELLED from server (no client cancellation tracer callback).
624+ generateServerInitiatedLoad (ImmutableMap .of (subchannel1 , Status .CANCELLED ), 7 );
625+
626+ // Move forward in time to a point where the detection timer has fired.
627+ forwardTime (config );
628+
629+ // Server-initiated CANCELLED status is counted as a failure, so subchannel1 should be ejected.
630+ assertEjectedSubchannels (ImmutableSet .of (ImmutableSet .copyOf (servers .get (0 ).getAddresses ())));
631+ }
632+
633+ /**
634+ * Server-initiated DEADLINE_EXCEEDED status over the wire (stopDelivery = false) counts
635+ * as failure and results in ejection under success rate algorithm.
636+ */
637+ @ Test
638+ public void successRateOneOutlier_serverInitiatedDeadlineExceededEjected () {
639+ OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig .Builder ()
640+ .setMaxEjectionPercent (50 )
641+ .setSuccessRateEjection (
642+ new SuccessRateEjection .Builder ()
643+ .setMinimumHosts (3 )
644+ .setRequestVolume (10 ).build ())
645+ .setChildConfig (newChildConfig (roundRobinLbProvider , null )).build ();
646+
647+ loadBalancer .acceptResolvedAddresses (buildResolvedAddress (config , servers ));
648+
649+ // subchannel1 returns DEADLINE_EXCEEDED from server (no client cancellation tracer callback).
650+ generateServerInitiatedLoad (ImmutableMap .of (subchannel1 , Status .DEADLINE_EXCEEDED ), 7 );
651+
652+ // Move forward in time to a point where the detection timer has fired.
653+ forwardTime (config );
654+
655+ // Server-initiated DEADLINE_EXCEEDED status is counted as a failure, so subchannel1 is ejected.
656+ assertEjectedSubchannels (ImmutableSet .of (ImmutableSet .copyOf (servers .get (0 ).getAddresses ())));
657+ }
658+
607659 /**
608660 * The success rate algorithm ejects the outlier, but then the config changes so that similar
609661 * behavior no longer gets ejected.
@@ -974,6 +1026,58 @@ public void failurePercentageOneOutlier_deadlineExceededIgnored() {
9741026 assertEjectedSubchannels (ImmutableSet .of ());
9751027 }
9761028
1029+ /**
1030+ * Server-initiated CANCELLED status over the wire (stopDelivery = false) counts as failure
1031+ * and results in ejection under failure percentage algorithm.
1032+ */
1033+ @ Test
1034+ public void failurePercentageOneOutlier_serverInitiatedCancelledEjected () {
1035+ OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig .Builder ()
1036+ .setMaxEjectionPercent (50 )
1037+ .setFailurePercentageEjection (
1038+ new FailurePercentageEjection .Builder ()
1039+ .setMinimumHosts (3 )
1040+ .setRequestVolume (10 ).build ())
1041+ .setChildConfig (newChildConfig (roundRobinLbProvider , null )).build ();
1042+
1043+ loadBalancer .acceptResolvedAddresses (buildResolvedAddress (config , servers ));
1044+
1045+ // subchannel1 returns CANCELLED from server (no client cancellation tracer callback).
1046+ generateServerInitiatedLoad (ImmutableMap .of (subchannel1 , Status .CANCELLED ), 7 );
1047+
1048+ // Move forward in time to a point where the detection timer has fired.
1049+ forwardTime (config );
1050+
1051+ // Server-initiated CANCELLED status is counted as a failure, so subchannel1 should be ejected.
1052+ assertEjectedSubchannels (ImmutableSet .of (ImmutableSet .copyOf (servers .get (0 ).getAddresses ())));
1053+ }
1054+
1055+ /**
1056+ * Server-initiated DEADLINE_EXCEEDED status over the wire (stopDelivery = false) counts
1057+ * as failure and results in ejection under failure percentage algorithm.
1058+ */
1059+ @ Test
1060+ public void failurePercentageOneOutlier_serverInitiatedDeadlineExceededEjected () {
1061+ OutlierDetectionLoadBalancerConfig config = new OutlierDetectionLoadBalancerConfig .Builder ()
1062+ .setMaxEjectionPercent (50 )
1063+ .setFailurePercentageEjection (
1064+ new FailurePercentageEjection .Builder ()
1065+ .setMinimumHosts (3 )
1066+ .setRequestVolume (10 ).build ())
1067+ .setChildConfig (newChildConfig (roundRobinLbProvider , null )).build ();
1068+
1069+ loadBalancer .acceptResolvedAddresses (buildResolvedAddress (config , servers ));
1070+
1071+ // subchannel1 returns DEADLINE_EXCEEDED from server (no client cancellation tracer callback).
1072+ generateServerInitiatedLoad (ImmutableMap .of (subchannel1 , Status .DEADLINE_EXCEEDED ), 7 );
1073+
1074+ // Move forward in time to a point where the detection timer has fired.
1075+ forwardTime (config );
1076+
1077+ // Server-initiated DEADLINE_EXCEEDED status is counted as a failure, so subchannel1 is ejected.
1078+ assertEjectedSubchannels (ImmutableSet .of (ImmutableSet .copyOf (servers .get (0 ).getAddresses ())));
1079+ }
1080+
9771081 /**
9781082 * The failure percentage algorithm ignores addresses without enough volume..
9791083 */
@@ -1542,6 +1646,36 @@ private void generateLoad(Map<Subchannel, Status> statusMap,
15421646 }
15431647 }
15441648
1649+ // Generates 100 calls, simulating server-initiated status responses over the wire.
1650+ private void generateServerInitiatedLoad (
1651+ Map <Subchannel , Status > statusMap , int expectedStateChanges ) {
1652+ deliverSubchannelState (subchannel1 , ConnectivityStateInfo .forNonError (READY ));
1653+ deliverSubchannelState (subchannel2 , ConnectivityStateInfo .forNonError (READY ));
1654+ deliverSubchannelState (subchannel3 , ConnectivityStateInfo .forNonError (READY ));
1655+ deliverSubchannelState (subchannel4 , ConnectivityStateInfo .forNonError (READY ));
1656+ deliverSubchannelState (subchannel5 , ConnectivityStateInfo .forNonError (READY ));
1657+
1658+ verify (mockHelper , times (expectedStateChanges )).updateBalancingState (stateCaptor .capture (),
1659+ pickerCaptor .capture ());
1660+ SubchannelPicker picker = pickerCaptor .getAllValues ()
1661+ .get (pickerCaptor .getAllValues ().size () - 1 );
1662+
1663+ HashMap <Subchannel , Integer > callCountMap = new HashMap <>();
1664+ for (int i = 0 ; i < 100 ; i ++) {
1665+ PickResult pickResult = picker
1666+ .pickSubchannel (mock (PickSubchannelArgs .class ));
1667+ ClientStreamTracer clientStreamTracer = pickResult .getStreamTracerFactory ()
1668+ .newClientStreamTracer (null , null );
1669+
1670+ Subchannel subchannel = (Subchannel ) pickResult .getSubchannel ().getInternalSubchannel ();
1671+
1672+ int calls = callCountMap .containsKey (subchannel ) ? callCountMap .get (subchannel ) : 0 ;
1673+ callCountMap .put (subchannel , ++calls );
1674+ Status status = statusMap .containsKey (subchannel ) ? statusMap .get (subchannel ) : Status .OK ;
1675+ clientStreamTracer .streamClosed (status );
1676+ }
1677+ }
1678+
15451679 // Forwards time past the moment when the timer will fire.
15461680 private void forwardTime (OutlierDetectionLoadBalancerConfig config ) {
15471681 fakeClock .forwardTime (config .intervalNanos + 1 , TimeUnit .NANOSECONDS );
0 commit comments