/* * Copyright 2012-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.util.network; import com.facebook.buck.log.Logger; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.io.CharStreams; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * HttpEndpoint implementation which only allows a certain number of concurrent requests to be in * flight at any given point in time. */ public class BlockingHttpEndpoint implements HttpEndpoint { private static final Logger LOG = Logger.get(BlockingHttpEndpoint.class); public static final int DEFAULT_COMMON_TIMEOUT_MS = 5000; private URL url; private int timeoutMillis; private final ListeningExecutorService requestService; private static final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat(BlockingHttpEndpoint.class.getSimpleName() + "-%d") .build(); public BlockingHttpEndpoint(String url, int maxParallelRequests, int timeoutMillis) throws MalformedURLException { this.url = new URL(url); this.timeoutMillis = timeoutMillis; // Create an ExecutorService that blocks after N requests are in flight. Taken from // http://www.springone2gx.com/blog/billy_newport/2011/05/there_s_more_to_configuring_threadpools_than_thread_pool_size LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(maxParallelRequests); ExecutorService executor = new ThreadPoolExecutor( maxParallelRequests, maxParallelRequests, 2L, TimeUnit.MINUTES, workQueue, threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); requestService = MoreExecutors.listeningDecorator(executor); } @Override public ListenableFuture<HttpResponse> post(final String content) { return requestService.submit( () -> { try { HttpURLConnection connection = buildConnection("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); return send(connection, content); } catch (IOException e) { throw new RuntimeException(e); } }); } @VisibleForTesting HttpResponse send(final HttpURLConnection connection, final String content) throws IOException { try (DataOutputStream out = new DataOutputStream(connection.getOutputStream())) { out.writeBytes(content); out.flush(); out.close(); InputStream inputStream = connection.getInputStream(); String response = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); return new HttpResponse(response); } finally { connection.disconnect(); } } private HttpURLConnection buildConnection(String httpMethod) throws IOException { HttpURLConnection connection = (HttpURLConnection) this.url.openConnection(); connection.setUseCaches(false); connection.setDoOutput(true); connection.setConnectTimeout(timeoutMillis); connection.setReadTimeout(timeoutMillis); connection.setRequestMethod(httpMethod); return connection; } /** * Attempt to complete submitted requests on close so that as much information is recorded as * possible. This aids debugging when close is called during exception processing. */ @Override public void close() { requestService.shutdown(); try { if (!requestService.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS)) { LOG.warn( Joiner.on(System.lineSeparator()) .join( "A BlockingHttpEndpoint failed to shut down within the standard timeout.", "Your build might have succeeded, but some requests made to ", this.url + " were probably lost.", "Here's some debugging information:", requestService.toString())); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }