|
16 | 16 |
|
17 | 17 | package io.grpc.xds;
|
18 | 18 |
|
| 19 | +import static com.google.common.base.Preconditions.checkNotNull; |
| 20 | +import static io.grpc.ConnectivityState.CONNECTING; |
| 21 | +import static io.grpc.ConnectivityState.IDLE; |
| 22 | +import static io.grpc.ConnectivityState.READY; |
| 23 | +import static io.grpc.ConnectivityState.TRANSIENT_FAILURE; |
| 24 | +import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER; |
| 25 | + |
| 26 | +import com.google.common.collect.ImmutableList; |
| 27 | +import com.google.common.collect.ImmutableMap; |
| 28 | +import io.grpc.ConnectivityState; |
| 29 | +import io.grpc.InternalLogId; |
19 | 30 | import io.grpc.LoadBalancer;
|
| 31 | +import io.grpc.MethodDescriptor; |
20 | 32 | import io.grpc.Status;
|
| 33 | +import io.grpc.internal.ServiceConfigUtil.PolicySelection; |
| 34 | +import io.grpc.util.ForwardingLoadBalancerHelper; |
| 35 | +import io.grpc.util.GracefulSwitchLoadBalancer; |
| 36 | +import io.grpc.xds.XdsLogger.XdsLogLevel; |
| 37 | +import io.grpc.xds.XdsRoutingLoadBalancerProvider.MethodName; |
| 38 | +import io.grpc.xds.XdsRoutingLoadBalancerProvider.Route; |
| 39 | +import io.grpc.xds.XdsRoutingLoadBalancerProvider.XdsRoutingConfig; |
| 40 | +import io.grpc.xds.XdsSubchannelPickers.ErrorPicker; |
| 41 | +import java.util.HashMap; |
| 42 | +import java.util.LinkedHashMap; |
| 43 | +import java.util.List; |
| 44 | +import java.util.Map; |
| 45 | +import javax.annotation.Nullable; |
21 | 46 |
|
22 |
| -// TODO(zdapeng): Implementation. |
23 | 47 | /** Load balancer for xds_routing policy. */
|
24 | 48 | final class XdsRoutingLoadBalancer extends LoadBalancer {
|
25 | 49 |
|
| 50 | + private final XdsLogger logger; |
| 51 | + private final Helper helper; |
| 52 | + private final Map<String, GracefulSwitchLoadBalancer> routeBalancers = new HashMap<>(); |
| 53 | + private final Map<String, RouteHelper> routeHelpers = new HashMap<>(); |
| 54 | + |
| 55 | + private Map<String, PolicySelection> actions = ImmutableMap.of(); |
| 56 | + private List<Route> routes = ImmutableList.of(); |
| 57 | + |
| 58 | + XdsRoutingLoadBalancer(Helper helper) { |
| 59 | + this.helper = checkNotNull(helper, "helper"); |
| 60 | + logger = XdsLogger.withLogId( |
| 61 | + InternalLogId.allocate("xds-routing-lb", helper.getAuthority())); |
| 62 | + logger.log(XdsLogLevel.INFO, "Created"); |
| 63 | + } |
| 64 | + |
| 65 | + @Override |
| 66 | + public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) { |
| 67 | + logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses); |
| 68 | + XdsRoutingConfig xdsRoutingConfig = |
| 69 | + (XdsRoutingConfig) resolvedAddresses.getLoadBalancingPolicyConfig(); |
| 70 | + checkNotNull(xdsRoutingConfig, "Missing xds_routing lb config"); |
| 71 | + |
| 72 | + Map<String, PolicySelection> newActions = xdsRoutingConfig.actions; |
| 73 | + for (String actionName : newActions.keySet()) { |
| 74 | + PolicySelection action = newActions.get(actionName); |
| 75 | + if (!actions.containsKey(actionName)) { |
| 76 | + RouteHelper routeHelper = new RouteHelper(); |
| 77 | + GracefulSwitchLoadBalancer routeBalancer = new GracefulSwitchLoadBalancer(routeHelper); |
| 78 | + routeBalancer.switchTo(action.getProvider()); |
| 79 | + routeHelpers.put(actionName, routeHelper); |
| 80 | + routeBalancers.put(actionName, routeBalancer); |
| 81 | + } else if (!action.getProvider().equals(actions.get(actionName).getProvider())) { |
| 82 | + routeBalancers.get(actionName).switchTo(action.getProvider()); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + this.routes = xdsRoutingConfig.routes; |
| 87 | + this.actions = newActions; |
| 88 | + |
| 89 | + for (String actionName : actions.keySet()) { |
| 90 | + routeBalancers.get(actionName).handleResolvedAddresses( |
| 91 | + resolvedAddresses.toBuilder() |
| 92 | + .setLoadBalancingPolicyConfig(actions.get(actionName).getConfig()) |
| 93 | + .build()); |
| 94 | + } |
| 95 | + |
| 96 | + // Cleanup removed actions. |
| 97 | + // TODO(zdapeng): cache removed actions for 15 minutes. |
| 98 | + for (String actionName : routeBalancers.keySet()) { |
| 99 | + if (!actions.containsKey(actionName)) { |
| 100 | + routeBalancers.get(actionName).shutdown(); |
| 101 | + } |
| 102 | + } |
| 103 | + routeBalancers.keySet().retainAll(actions.keySet()); |
| 104 | + routeHelpers.keySet().retainAll(actions.keySet()); |
| 105 | + } |
| 106 | + |
26 | 107 | @Override
|
27 | 108 | public void handleNameResolutionError(Status error) {
|
| 109 | + logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error); |
| 110 | + if (routeBalancers.isEmpty()) { |
| 111 | + helper.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(error)); |
| 112 | + } |
| 113 | + for (LoadBalancer routeBalancer : routeBalancers.values()) { |
| 114 | + routeBalancer.handleNameResolutionError(error); |
| 115 | + } |
28 | 116 | }
|
29 | 117 |
|
30 | 118 | @Override
|
31 | 119 | public void shutdown() {
|
| 120 | + logger.log(XdsLogLevel.INFO, "Shutdown"); |
| 121 | + for (LoadBalancer routeBalancer : routeBalancers.values()) { |
| 122 | + routeBalancer.shutdown(); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + @Override |
| 127 | + public boolean canHandleEmptyAddressListFromNameResolution() { |
| 128 | + return true; |
| 129 | + } |
| 130 | + |
| 131 | + private void updateOverallBalancingState() { |
| 132 | + ConnectivityState overallState = null; |
| 133 | + // Use LinkedHashMap to preserve the order of routes. |
| 134 | + Map<MethodName, SubchannelPicker> routePickers = new LinkedHashMap<>(); |
| 135 | + for (Route route : routes) { |
| 136 | + RouteHelper routeHelper = routeHelpers.get(route.actionName); |
| 137 | + routePickers.put(route.methodName, routeHelper.currentPicker); |
| 138 | + ConnectivityState routeState = routeHelper.currentState; |
| 139 | + overallState = aggregateState(overallState, routeState); |
| 140 | + } |
| 141 | + if (overallState != null) { |
| 142 | + SubchannelPicker picker = new PathMatchingSubchannelPicker(routePickers); |
| 143 | + helper.updateBalancingState(overallState, picker); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + @Nullable |
| 148 | + private static ConnectivityState aggregateState( |
| 149 | + @Nullable ConnectivityState overallState, ConnectivityState childState) { |
| 150 | + if (overallState == null) { |
| 151 | + return childState; |
| 152 | + } |
| 153 | + if (overallState == READY || childState == READY) { |
| 154 | + return READY; |
| 155 | + } |
| 156 | + if (overallState == CONNECTING || childState == CONNECTING) { |
| 157 | + return CONNECTING; |
| 158 | + } |
| 159 | + if (overallState == IDLE || childState == IDLE) { |
| 160 | + return IDLE; |
| 161 | + } |
| 162 | + return overallState; |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * The lb helper for a single route balancer. |
| 167 | + */ |
| 168 | + private final class RouteHelper extends ForwardingLoadBalancerHelper { |
| 169 | + ConnectivityState currentState = CONNECTING; |
| 170 | + SubchannelPicker currentPicker = BUFFER_PICKER; |
| 171 | + |
| 172 | + @Override |
| 173 | + public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) { |
| 174 | + currentState = newState; |
| 175 | + currentPicker = newPicker; |
| 176 | + updateOverallBalancingState(); |
| 177 | + } |
| 178 | + |
| 179 | + @Override |
| 180 | + protected Helper delegate() { |
| 181 | + return helper; |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + private static final class PathMatchingSubchannelPicker extends SubchannelPicker { |
| 186 | + |
| 187 | + final Map<MethodName, SubchannelPicker> routePickers; |
| 188 | + |
| 189 | + /** |
| 190 | + * Constructs a picker that will match the path of PickSubchannelArgs with the given map. |
| 191 | + * The order of the map entries matters. First match will be picked even if second match is an |
| 192 | + * exact (service + method) path match. |
| 193 | + */ |
| 194 | + PathMatchingSubchannelPicker(Map<MethodName, SubchannelPicker> routePickers) { |
| 195 | + this.routePickers = routePickers; |
| 196 | + } |
| 197 | + |
| 198 | + @Override |
| 199 | + public PickResult pickSubchannel(PickSubchannelArgs args) { |
| 200 | + for (MethodName methodName : routePickers.keySet()) { |
| 201 | + if (match(args.getMethodDescriptor(), methodName)) { |
| 202 | + return routePickers.get(methodName).pickSubchannel(args); |
| 203 | + } |
| 204 | + } |
| 205 | + // At least the default route should match, otherwise there is a bug. |
| 206 | + throw new IllegalStateException("PathMatchingSubchannelPicker: error in matching path"); |
| 207 | + } |
| 208 | + |
| 209 | + boolean match(MethodDescriptor<?, ?> methodDescriptor, MethodName methodName) { |
| 210 | + if (methodName.service.isEmpty() && methodName.method.isEmpty()) { |
| 211 | + return true; |
| 212 | + } |
| 213 | + if (methodName.method.isEmpty()) { |
| 214 | + return methodName.service.equals(methodDescriptor.getServiceName()); |
| 215 | + } |
| 216 | + return (methodName.service + '/' + methodName.method) |
| 217 | + .equals(methodDescriptor.getFullMethodName()); |
| 218 | + } |
32 | 219 | }
|
33 | 220 | }
|
0 commit comments