RetryAndFollowUpInterceptor explained by OKHttp principle

1. Preface:

1.1 The main content of this article

1. Introduction of main members in RetryAndFollowUpInterceptor

2. Retry mechanism in interceptor

3. Execution process in the interceptor

1.2 OKHttp project address:

https://github.com/square/okhttp

1.3 Introduction to the OKHttp series:

The OKHttp project itself is still very large, and involves a lot of knowledge points, so it is difficult to summarize the whole article in one article, so I will explain OKHttp in detail in a series of articles.

The series is going to write the following articles.

The basic concept of OKHttp principle explanation - Lost Summer Blog - CSDN Blog

Chain of Responsibility Mode and Extension of OKHttp Principle Explanation - Lost Summer Blog - CSDN Blog

Retry interceptor explained by OKHttp principle (expected to be released on 03.22)

Routing interceptor explained by OKHttp principle

Cache interceptor explained by OKHttp principle

Connection pool interceptor explained by OKHttp principle

Request interceptor explained by OKHttp principle

PS: It is expected that the progress of 1-2 articles will be updated every week.

2. Introduction of key members

Route : Route. When we request a service, we need to return multiple IPs through DNS domain name resolution for us to try to connect. And there may be multiple layers of DNS in the middle,

StreamAllocation: StreamAllocation wrote in its comments, mainly to coordinate the relationship between Connections, Streams and Call.

StreamAllocation contains several main objects, as follows:

ConnectionPool : Connection pool, which will be discussed in detail in the ConnectInterceptor chapter. After a request ends, the connection will not be closed immediately, and the socket can also be used to continue data transmission.

Address : The encapsulation object of the request address.

Call : request body

EventListener : Listening object, convenient for monitoring status

RouteSelector : The route selector. When we request a service, we need to go through DNS domain name resolution to return multiple

3. Retry mechanism in RetryAndFollowUpInterceptor

3.1 Retry mechanism

I feel that the retry mechanism here is particularly clever. First, the outer layer is a while(true) loop, and then the request is sent. If there is a problem with the return value or an exception is thrown, the number of retries will be consumed. If the upper limit of the number of retries is not exceeded, the request will be sent in a loop.

If the current request is successful, and if the communication is performed (no cache is used), the current link and resource are closed and released through streamAllocation.release();.

If it fails, also close the data stream in the response.

//代码3.1
while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        ...
        //代码3.2
        continue;
      } catch (IOException e) {
        ...
        continue;
      } finally {
        ...
      }
      ...
      Request followUp = followUpRequest(response, streamAllocation.route());

      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      closeQuietly(response.body());

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }

      request = followUp;
      priorResponse = response;
    }

3.2 How to judge whether the request is successful?

Even if the request is successful, the request may not be actually sent, because there is still the possibility of hitting the cache. So this requires comprehensive judgment. In the interceptor, the judgment method is in the followUpRequest method.

The design here is very clever. If the request returned by the followUpRequest method is not empty, it means that the request fails. If the return value of followUp is not empty in the next request, it means that the result is OK. Otherwise, the next request is the return value of followUp this time.

In addition, we looked at the code in detail and found that there is no 200, 404 that we commonly know in this judgment method. Why?

We can see the above code segment 3.2, where exception capture is performed, in fact, the principle is here. If it is a 404 error, an exception will be thrown directly in the subsequent iterator, and caught at 3.2. So it will not be executed to the followUpRequest method that follows.

What if it was 200 successes? If responseCode=200, it will go to the logic of code 3.3 and return null directly. Thus the representative is that the response is in compliance with the requirements.

private Request followUpRequest(Response userResponse, Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      case HTTP_PROXY_AUTH:
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);

      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Most redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if (!client.retryOnConnectionFailure()) {
          // The application layer has directed us not to retry the request.
          return null;
        }

        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          return null;
        }

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();

      case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        //代码3.3
        return null;
    }
  }

Guess you like

Origin blog.csdn.net/AA5279AA/article/details/123583835