001 /*
002 * Copyright 2010-2011 Ning, Inc.
003 *
004 * Ning licenses this file to you under the Apache License, version 2.0
005 * (the "License"); you may not use this file except in compliance with the
006 * License. You may obtain a copy of the License at:
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations
014 * under the License.
015 */
016
017 package com.ning.metrics.eventtracker;
018
019 import com.ning.http.client.AsyncCompletionHandler;
020 import com.ning.http.client.AsyncHttpClient;
021 import com.ning.http.client.AsyncHttpClientConfig;
022 import com.ning.http.client.Request;
023 import com.ning.http.client.Response;
024 import com.ning.metrics.serialization.writer.CallbackHandler;
025 import com.yammer.metrics.Metrics;
026 import com.yammer.metrics.core.TimerMetric;
027 import org.slf4j.Logger;
028 import org.slf4j.LoggerFactory;
029
030 import java.io.File;
031 import java.io.IOException;
032 import java.util.HashMap;
033 import java.util.Map;
034 import java.util.concurrent.TimeUnit;
035 import java.util.concurrent.atomic.AtomicLong;
036
037 public class HttpSender implements EventSender
038 {
039 private static final Logger log = LoggerFactory.getLogger(HttpSender.class);
040 private static final Map<EventType, String> headers = new HashMap<EventType, String>();
041
042 private final AtomicLong activeRequests = new AtomicLong(0);
043 private final TimerMetric sendTimer;
044
045 static {
046 headers.put(EventType.SMILE, "application/json+smile");
047 headers.put(EventType.JSON, "application/json");
048 headers.put(EventType.THRIFT, "ning/thrift");
049 headers.put(EventType.DEFAULT, "ning/1.0");
050 }
051
052 public static final String URI_PATH = "/rest/1.0/event";
053 private static final int DEFAULT_IDLE_CONNECTION_IN_POOL_TIMEOUT_IN_MS = 120000; // 2 minutes
054
055 private final EventType eventType;
056 private final long httpMaxWaitTimeInMillis;
057 private final String collectorURI;
058 private final AsyncHttpClientConfig clientConfig;
059
060 private AsyncHttpClient client;
061
062 public HttpSender(final String collectorHost, final int collectorPort, final EventType eventType, final long httpMaxWaitTimeInMillis)
063 {
064 this.eventType = eventType;
065 this.httpMaxWaitTimeInMillis = httpMaxWaitTimeInMillis;
066 collectorURI = String.format("http://%s:%d%s", collectorHost, collectorPort, URI_PATH);
067 sendTimer = Metrics.newTimer(HttpSender.class, collectorURI, TimeUnit.MILLISECONDS, TimeUnit.SECONDS);
068 // CAUTION: it is not enforced that the actual event encoding type on the wire matches what the config says it is
069 // the event encoding type is determined by the Event's writeExternal() method.
070 clientConfig = new AsyncHttpClientConfig.Builder()
071 .setIdleConnectionInPoolTimeoutInMs(DEFAULT_IDLE_CONNECTION_IN_POOL_TIMEOUT_IN_MS)
072 .setConnectionTimeoutInMs(100)
073 .setMaximumConnectionsPerHost(-1) // unlimited connections
074 .build();
075 }
076
077 /**
078 * Send a file full of events to the collector. This does zero-bytes-copy by default (the async-http-client does
079 * it for us).
080 *
081 * @param file File to send
082 * @param handler callback handler for the serialization-writer library
083 */
084 @Override
085 public void send(final File file, final CallbackHandler handler)
086 {
087 if (client == null || client.isClosed()) {
088 client = new AsyncHttpClient(clientConfig);
089 }
090
091 try {
092 log.info("Sending local file to collector: {}", file.getAbsolutePath());
093
094 final long startTime = System.nanoTime();
095 client.executeRequest(createPostRequest(file),
096 new AsyncCompletionHandler<Response>()
097 {
098 @Override
099 public Response onCompleted(final Response response)
100 {
101 activeRequests.decrementAndGet();
102
103 if (response.getStatusCode() == 202) {
104 handler.onSuccess(file);
105 }
106 else {
107 handler.onError(new IOException(String.format("Received response %d: %s",
108 response.getStatusCode(), response.getStatusText())), file);
109 }
110
111 sendTimer.update(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
112 return response; // never read
113 }
114
115 @Override
116 public void onThrowable(final Throwable t)
117 {
118 activeRequests.decrementAndGet();
119 handler.onError(t, file);
120 }
121 });
122
123 activeRequests.incrementAndGet();
124 }
125 catch (IOException e) {
126 // Recycle the client
127 client.close();
128 handler.onError(e, file);
129 }
130 }
131
132 @Override
133 public synchronized void close()
134 {
135 if (client != null && !client.isClosed()) {
136 try {
137 if (activeRequests.get() > 0) {
138 log.info(String.format("%d HTTP request(s) in progress, giving them some time to finish...", activeRequests.get()));
139 }
140
141 long sleptInMillins = httpMaxWaitTimeInMillis;
142 while (activeRequests.get() > 0 && sleptInMillins >= 0) {
143 Thread.sleep(200);
144 sleptInMillins -= 200;
145 }
146
147 if (activeRequests.get() > 0) {
148 log.warn("Giving up on pending HTTP requests, shutting down NOW!");
149 }
150 }
151 catch (InterruptedException e) {
152 log.warn("Interrupted while waiting for active queries to finish");
153 Thread.currentThread().interrupt();
154 }
155 finally {
156 client.close();
157 }
158 }
159 }
160
161 private Request createPostRequest(final File file)
162 {
163 final AsyncHttpClient.BoundRequestBuilder requestBuilder = client
164 .preparePost(collectorURI).setBody(file)
165 .setHeader("Content-Type", headers.get(eventType)); // zero-bytes-copy
166 return requestBuilder.build();
167 }
168 }