web-dev-qa-db-ja.com

RestTemplateはリクエストごとにタイムアウトを設定します

私は@Serviceいくつかのメソッドでは、各メソッドは異なるWebAPIを消費します。各呼び出しには、カスタム読み取りタイムアウトが必要です。 RestTemplateインスタンスを1つ持ち、そのように各メソッドでファクトリを介してタイムアウトを変更することはスレッドセーフですか?

((HttpComponentsClientHttpRequestFactory)restTemplate.getRequestFactory())
.setReadTimeout(customMillis);

私の懸念は、ファクトリのタイムアウトを変更していることであり、それはRequestConfigのようではありません。これらのメソッドが複数のユーザーによって同時に呼び出される可能性があることを考慮すると、このアプローチはスレッドセーフですか?または、各メソッドに独自のRestTemplateが必要ですか?

4
prettyvoid

RestTemplateの初期化後にファクトリからタイムアウトを変更することは、発生するのを待っている単なる競合状態です(Todd explained のように)。 RestTemplateは、事前に構成されたタイムアウトを使用して構築され、初期化後もそれらのタイムアウトが変更されないように設計されています。 Apache HttpClientを使用する場合は、はい、リクエストごとにRequestConfigを設定できます。これは、私の意見では適切な設計です。

私たちはすでにプロジェクトの至る所でRestTemplateを使用しており、現時点ではhttpクライアントの切り替えが発生するリファクタリングを行う余裕はありません。

今のところ、私はRestTemplateプーリングソリューションに行き着き、RestTemplateManagerというクラスを作成し、テンプレートの作成とプールのすべての責任をそれに与えました。このマネージャーには、サービスとreadTimeoutによってグループ化されたテンプレートのローカルキャッシュがあります。次の構造のキャッシュハッシュマップを想像してみてください。

ServiceA | 1000-> RestTemplate

ServiceA | 3000-> RestTemplate

ServiceB | 1000-> RestTemplate

キーの数値はミリ秒単位のreadTimeoutです(キーは後でreadTimeout以上をサポートするように調整できます)。したがって、ServiceAが読み取りタイムアウトが1000ミリ秒のテンプレートを要求すると、マネージャーはキャッシュされたインスタンスを返します。存在しない場合は、作成されて返されます。

このアプローチでは、RestTemplatesを事前に定義する必要がなくなり、上記のマネージャーにRestTemplateを要求するだけで済みます。これにより、初期化も最小限に抑えられます。

これは、RestTemplateを破棄して、より適切なソリューションを使用する時間があるまで実行されます。

3
prettyvoid

応答に時間がかかりすぎる場合に備えて、読み取りタイムアウトが必要だと思います。

考えられる解決策は、指定された時間内にリクエストが完了しなかった場合にリクエストをキャンセルして、タイムアウトを自分で実装することです。

これを実現するには、代わりにAsyncRestTemplateを使用できます。これには、タイムアウトやキャンセルなどの非同期操作のサポートが組み込まれています。

これにより、各リクエストのタイムアウトをより細かく制御できます。例:

ListenableFuture<ResponseEntity<Potato>> future =
                asyncRestTemplate.getForEntity(url, Potato.class);

ResponseEntity<Potato> response = future.get(5, TimeUnit.SECONDS);
4
ESala

オプション1:複数のRestTemplate

作成された接続のプロパティを変更する場合は、構成ごとに1つのRestTemplateが必要になります。最近、これとまったく同じ問題が発生し、RestTemplateの2つのバージョンがありました。1つは「短いタイムアウト」用で、もう1つは「長いタイムアウト」用です。各グループ(短い/長い)内で、私はそのRestTemplateを共有することができました。

呼び出しでタイムアウト設定を変更し、接続を作成します。最良の結果を期待するのは、競合状態が発生するのを待っていることです。私はこれを安全にプレイし、複数のRestTemplateを作成します。

例:

@Configuration
public class RestTemplateConfigs {
    @Bean("shortTimeoutRestTemplate")
    public RestTemplate shortTimeoutRestTemplate() {
       // Create template with short timeout, see docs.
    }
    @Bean("longTimeoutRestTemplate")
    public RestTemplate longTimeoutRestTemplate() {
       // Create template with short timeout, see docs.
    }
}

そして、必要に応じてそれらをサービスに接続できます。

@Service
public class MyService {
    private final RestTemplate shortTimeout;
    private final RestTemplate longTimeout;

    @Autowired
    public MyService(@Qualifier("shortTimeoutRestTemplate") RestTemplate shortTimeout, 
                     @Qualifier("longTimeoutRestTemplate") RestTemplate longTimeout) {
        this.shortTimeout = shortTimeout;
        this.longTimeout = longTimeout;
    }

    // Your business methods here...
}

オプション2:サーキットブレーカーで呼び出しをラップする

外部サービスを呼び出す場合は、おそらく 回路ブレーカーを使用する必要があります これに使用します。 Spring Bootは、サーキットブレーカーパターンの一般的な実装であるHystrixでうまく機能します。 hystrixを使用すると、呼び出す各サービスのフォールバックとタイムアウトを制御できます。

サービスAに2つのオプションがあるとします:1)安いが時々遅い2)高価だが速い。 Hystrixを使用して、Cheap/Slowをあきらめ、本当に必要なときにExpensive/Fastを使用できます。または、バックアップを作成せず、Hystrixに適切なデフォルトを提供するメソッドを呼び出させることもできます。

テストされていない例:

@EnableCircuitBreaker
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp .class, args);
    }
}

@Service
public class MyService {
    private final RestTemplate restTemplate;

    public BookService(RestTemplate rest) {
        this.restTemplate = rest;
    }

    @HystrixCommand(
        fallbackMethod = "fooMethodFallback",
        commandProperties = { 
            @HystrixProperty(
                 name = "execution.isolation.thread.timeoutInMilliseconds", 
                 value="5000"
            )
        }
    )
    public String fooMethod() {
        // Your logic here.
        restTemplate.exchange(...); 
    }

    public String fooMethodFallback(Throwable t) {
        log.error("Fallback happened", t);
        return "Sensible Default Here!"
    }
}

フォールバック方法にもオプションがあります。 thatメソッドに@HystrixCommandで注釈を付けて、別のサービス呼び出しを試みることができます。または、適切なデフォルトを指定することもできます。

3
Todd

私は自分でこの問題に遭遇したばかりで、周りを検索してもうまくいったと感じた解決策は見つかりませんでした。これが私の解決策とその背後にある思考プロセスです。

HttpComponentsClientHttpRequestFactoryを使用して、RestTemplateのタイムアウトを設定します。リクエストを行うたびに、内部的にはrequestFactoryのcreateRequest関数が呼び出されます。ここにタイムアウトがあり、いくつかのリクエスト固有のプロパティが設定されているRequestConfigがあります。次に、このRequestConfigはHttpContextに設定されます。以下は、このRequestConfigとHttpContextを構築するために実行される手順(順序)です。

  1. HttpComponentsClientHttpRequestFactory内でcreateHttpContext関数を呼び出します。この関数は、デフォルトでは何も行わず、nullを返します。
  2. RequestConfigが存在する場合はHttpUriRequestから取得し、HttpContextに追加します。
  3. HttpClientからRequestConfigを内部的に取得するHttpComponentsClientHttpRequestFactory内でcreateRequestConfig関数を呼び出し、それをrequestFactoryに内部的に構築されたRequestConfigとマージして、HttpContextに追加します。 (デフォルトでは、これが発生します)

私の意見では、これら3つすべてにソリューションを構築できます。最も簡単で堅牢なソリューションは、#1を中心にソリューションを構築することだと思います。最終的に独自のHttpComponentsRequestFactoryを作成し、内部にロジックを持つcreateHttpContext関数をオーバーライドして、リクエストURIのパスがそのpathPatternに指定されたタイムアウトで指定したpathPatternと一致するかどうかを確認しました。

public class PathTimeoutHttpComponentsClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {
  private List<PathPatternTimeoutConfig> pathPatternTimeoutConfigs = new ArrayList<>();

  protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
    for (PathPatternTimeoutConfig config : pathPatternTimeoutConfigs) {
      if (httpMethod.equals(config.getHttpMethod())) {
        final Matcher matcher = config.getPattern().matcher(uri.getPath());
        if (matcher.matches()) {
          HttpClientContext context = HttpClientContext.create();
          RequestConfig requestConfig = createRequestConfig(getHttpClient());  // Get default request config and modify timeouts as specified
          requestConfig = RequestConfig.copy(requestConfig)
              .setSocketTimeout(config.getReadTimeout())
              .setConnectTimeout(config.getConnectionTimeout())
              .setConnectionRequestTimeout(config.getConnectionRequestTimeout())
              .build();
          context.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);
          return context;
        }
      }
    }

    // Returning null allows HttpComponentsClientHttpRequestFactory to continue down normal path for populating the context
    return null;
  }

  public void addPathTimeout(HttpMethod httpMethod, String pathPattern, int connectionTimeout, int connectionRequestTimeout, int readTimeout) {
    Assert.hasText(pathPattern, "pathPattern must not be null, empty, or blank");
    final PathPatternTimeoutConfig pathPatternTimeoutConfig = new PathPatternTimeoutConfig(httpMethod, pathPattern, connectionTimeout, connectionRequestTimeout, readTimeout);
    pathPatternTimeoutConfigs.add(pathPatternTimeoutConfig);
  }

  private class PathPatternTimeoutConfig {
    private HttpMethod httpMethod;
    private String pathPattern;
    private int connectionTimeout;
    private int connectionRequestTimeout;
    private int readTimeout;
    private Pattern pattern;

    public PathPatternTimeoutConfig(HttpMethod httpMethod, String pathPattern, int connectionTimeout, int connectionRequestTimeout, int readTimeout) {
      this.httpMethod = httpMethod;
      this.pathPattern = pathPattern;
      this.connectionTimeout = connectionTimeout;
      this.connectionRequestTimeout = connectionRequestTimeout;
      this.readTimeout = readTimeout;
      this.pattern = Pattern.compile(pathPattern);
    }

    public HttpMethod getHttpMethod() {
      return httpMethod;
    }

    public String getPathPattern() {
      return pathPattern;
    }

    public int getConnectionTimeout() {
      return connectionTimeout;
    }

    public int getConnectionRequestTimeout() { return connectionRequestTimeout; }

    public int getReadTimeout() {
      return readTimeout;
    }

    public Pattern getPattern() {
      return pattern;
    }
  }
}

次に、必要に応じて、デフォルトのタイムアウトを使用してこのリクエストファクトリのインスタンスを作成し、このような特定のパスにカスタムタイムアウトを指定できます。

@Bean
public PathTimeoutHttpComponentsClientHttpRequestFactory requestFactory() {
  final PathTimeoutHttpComponentsClientHttpRequestFactory factory = new PathTimeoutHttpComponentsClientHttpRequestFactory();
  factory.addPathTimeout(HttpMethod.POST, "\\/api\\/groups\\/\\d+\\/users\\/\\d+", 1000, 1000, 30000); // 30 second read timeout instead of 5
  factory.setConnectionRequestTimeout(1000);
  factory.setConnectTimeout(1000);
  factory.setReadTimeout(5000);
  return factory;
}

@Bean
public RestTemplate restTemplate() {
  final RestTemplate restTemplate = new RestTemplate();
  restTemplate.setRequestFactory(requestFactory());
  ...
  return restTemplate;
}

このアプローチは非常に再利用可能であり、一意のタイムアウトごとに個別のRestTemplateを作成する必要はなく、私が知る限り、スレッドセーフです。

1
Steve W