1 /* 2 * Copyright 2023 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.internal; 18 19 import com.google.common.annotations.VisibleForTesting; 20 import io.grpc.Attributes; 21 import io.grpc.NameResolver; 22 import io.grpc.Status; 23 import io.grpc.SynchronizationContext; 24 25 /** 26 * This wrapper class can add retry capability to any polling {@link NameResolver} implementation 27 * that supports calling {@link ResolutionResultListener}s with the outcome of each resolution. 28 * 29 * <p>The {@link NameResolver} used with this 30 */ 31 final class RetryingNameResolver extends ForwardingNameResolver { 32 33 private final NameResolver retriedNameResolver; 34 private final RetryScheduler retryScheduler; 35 private final SynchronizationContext syncContext; 36 37 static final Attributes.Key<ResolutionResultListener> RESOLUTION_RESULT_LISTENER_KEY 38 = Attributes.Key.create( 39 "io.grpc.internal.RetryingNameResolver.RESOLUTION_RESULT_LISTENER_KEY"); 40 41 /** 42 * Creates a new {@link RetryingNameResolver}. 43 * 44 * @param retriedNameResolver A {@link NameResolver} that will have failed attempt retried. 45 * @param retryScheduler Used to schedule the retry attempts. 46 */ RetryingNameResolver(NameResolver retriedNameResolver, RetryScheduler retryScheduler, SynchronizationContext syncContext)47 RetryingNameResolver(NameResolver retriedNameResolver, RetryScheduler retryScheduler, 48 SynchronizationContext syncContext) { 49 super(retriedNameResolver); 50 this.retriedNameResolver = retriedNameResolver; 51 this.retryScheduler = retryScheduler; 52 this.syncContext = syncContext; 53 } 54 55 @Override start(Listener2 listener)56 public void start(Listener2 listener) { 57 super.start(new RetryingListener(listener)); 58 } 59 60 @Override shutdown()61 public void shutdown() { 62 super.shutdown(); 63 retryScheduler.reset(); 64 } 65 66 /** 67 * Used to get the underlying {@link NameResolver} that is getting its failed attempts retried. 68 */ 69 @VisibleForTesting getRetriedNameResolver()70 NameResolver getRetriedNameResolver() { 71 return retriedNameResolver; 72 } 73 74 @VisibleForTesting 75 class DelayedNameResolverRefresh implements Runnable { 76 @Override run()77 public void run() { 78 refresh(); 79 } 80 } 81 82 private class RetryingListener extends Listener2 { 83 private Listener2 delegateListener; 84 RetryingListener(Listener2 delegateListener)85 RetryingListener(Listener2 delegateListener) { 86 this.delegateListener = delegateListener; 87 } 88 89 @Override onResult(ResolutionResult resolutionResult)90 public void onResult(ResolutionResult resolutionResult) { 91 // If the resolution result listener is already an attribute it indicates that a name resolver 92 // has already been wrapped with this class. This indicates a misconfiguration. 93 if (resolutionResult.getAttributes().get(RESOLUTION_RESULT_LISTENER_KEY) != null) { 94 throw new IllegalStateException( 95 "RetryingNameResolver can only be used once to wrap a NameResolver"); 96 } 97 98 delegateListener.onResult(resolutionResult.toBuilder().setAttributes( 99 resolutionResult.getAttributes().toBuilder() 100 .set(RESOLUTION_RESULT_LISTENER_KEY, new ResolutionResultListener()).build()) 101 .build()); 102 } 103 104 @Override onError(Status error)105 public void onError(Status error) { 106 delegateListener.onError(error); 107 syncContext.execute(() -> retryScheduler.schedule(new DelayedNameResolverRefresh())); 108 } 109 } 110 111 /** 112 * Simple callback class to store in {@link ResolutionResult} attributes so that 113 * ManagedChannel can indicate if the resolved addresses were accepted. Temporary until 114 * the Listener2.onResult() API can be changed to return a boolean for this purpose. 115 */ 116 class ResolutionResultListener { resolutionAttempted(boolean successful)117 public void resolutionAttempted(boolean successful) { 118 if (successful) { 119 retryScheduler.reset(); 120 } else { 121 retryScheduler.schedule(new DelayedNameResolverRefresh()); 122 } 123 } 124 } 125 } 126