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    }