/* * Copyright 2010-2012 Ning, Inc. * * Ning licenses this file to you 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.ning.metrics.eventtracker; import com.ning.http.client.AsyncCompletionHandler; import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClientConfig; import com.ning.http.client.Request; import com.ning.http.client.Response; import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; /** * Thread-safe wrapper class around the AsyncHttpClient which has weak synchronization contracts * between the client itself and the underlying providers (e.g. on close). */ public class ThreadSafeAsyncHttpClient { private static final String URI_PATH = "/rest/1.0/event"; private static final int DEFAULT_IDLE_CONNECTION_IN_POOL_TIMEOUT_IN_MS = 120000; // 2 minutes private static final Map<EventType, String> headers = new HashMap<EventType, String>(); static { headers.put(EventType.SMILE, "application/json+smile"); headers.put(EventType.JSON, "application/json"); headers.put(EventType.THRIFT, "ning/thrift"); headers.put(EventType.DEFAULT, "ning/1.0"); } private final AtomicBoolean isClosed = new AtomicBoolean(false); private final String collectorURI; private final EventType eventType; private final AsyncHttpClientConfig clientConfig; /** * Timer used for ensuring we close persistent HTTP connections every now * and then. */ private final ExpirationTimer httpConnectionExpiration; private AsyncHttpClient client = null; public ThreadSafeAsyncHttpClient(final String collectorHost, final int collectorPort, final EventType eventType, final long httpMaxKeepAliveInMillis) { this.collectorURI = String.format("http://%s:%d%s", collectorHost, collectorPort, URI_PATH); this.eventType = eventType; this.httpConnectionExpiration = new ExpirationTimer(httpMaxKeepAliveInMillis); // CAUTION: it is not enforced that the actual event encoding type on the wire matches what the config says it is // the event encoding type is determined by the Event's writeExternal() method. this.clientConfig = new AsyncHttpClientConfig.Builder() .setIdleConnectionInPoolTimeoutInMs(DEFAULT_IDLE_CONNECTION_IN_POOL_TIMEOUT_IN_MS) .setConnectionTimeoutInMs(100) .setMaximumConnectionsPerHost(-1) // unlimited connections .build(); } public synchronized void executeRequest(final File file, final AsyncCompletionHandler<Response> completionHandler) { if (isClosed.get()) { return; } if (client == null || client.isClosed()) { client = createClient(); } final Request request = createPostRequest(file); try { client.executeRequest(request, completionHandler); } catch (Exception e) { // Recycle the client on IOException and RuntimeExceptions client.close(); client = createClient(); completionHandler.onThrowable(e); } } public synchronized void close() { isClosed.set(true); if (client != null) { client.close(); } } synchronized AsyncHttpClient createClient() { return new AsyncHttpClient(clientConfig); } Request createPostRequest(final File file) { AsyncHttpClient.BoundRequestBuilder requestBuilder = client .preparePost(collectorURI).setBody(file) .setHeader("Content-Type", headers.get(eventType)); // zero-bytes-copy /* * Need to ensure we won't be using a single connection indefinitely, * to ensure load balancing works. */ if (httpConnectionExpiration.isExpired()) { requestBuilder = requestBuilder.setHeader("Connection", "close"); } return requestBuilder.build(); } }