/*
* Copyright 2011-2014 Proofpoint, 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.proofpoint.event.collector;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Sets;
import com.proofpoint.http.client.HttpClient;
import com.proofpoint.http.client.Request;
import com.proofpoint.http.client.Response;
import com.proofpoint.http.client.ResponseHandler;
import com.proofpoint.json.JsonCodec;
import com.proofpoint.log.Logger;
import com.proofpoint.units.Duration;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import java.net.URI;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.proofpoint.event.collector.EventCollectorStats.Status.DELIVERED;
import static com.proofpoint.event.collector.EventCollectorStats.Status.DROPPED;
import static com.proofpoint.event.collector.EventCollectorStats.Status.LOST;
import static com.proofpoint.event.collector.EventCollectorStats.Status.REJECTED;
import static com.proofpoint.http.client.JsonBodyGenerator.jsonBodyGenerator;
import static java.lang.String.format;
import static java.lang.Thread.sleep;
import static javax.ws.rs.core.Response.Status.Family.CLIENT_ERROR;
import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL;
import static javax.ws.rs.core.Response.Status.fromStatusCode;
class HttpEventTapFlow implements EventTapFlow
{
private static final Logger log = Logger.get(HttpEventTapFlow.class);
private static final Random RANDOM = new Random();
private static final String QOS_HEADER = "X-Proofpoint-QoS";
private static final String QOS_HEADER_FIRST_BATCH = "firstBatch";
private static final String QOS_HEADER_DROPPED_ENTRIES = "droppedMessages=%d";
private final HttpClient httpClient;
private final JsonCodec<List<Event>> eventsCodec;
private final long retryDelayMillis;
private final int retryCount;
private final String eventType;
private final String flowId;
private final AtomicReference<List<URI>> taps = new AtomicReference<List<URI>>(ImmutableList.<URI>of());
private final Set<URI> unestablishedTaps = Sets.newSetFromMap(new MapMaker().<URI, Boolean>makeMap());
private final AtomicLong droppedEntries = new AtomicLong(0);
private final EventCollectorStats eventCollectorStats;
public HttpEventTapFlow(HttpClient httpClient, JsonCodec<List<Event>> eventsCodec,
String eventType, String flowId, Set<URI> taps, int retryCount, Duration retryDelay, EventCollectorStats eventCollectorStats)
{
this.eventCollectorStats = checkNotNull(eventCollectorStats, "eventCollectorStats is null");
this.httpClient = checkNotNull(httpClient, "httpClient is null");
this.eventsCodec = checkNotNull(eventsCodec, "eventsCodec is null");
this.eventType = checkNotNull(eventType, "eventType is null");
this.flowId = checkNotNull(flowId, "flowId is null");
this.retryCount = retryCount;
if (this.retryCount > 0) {
this.retryDelayMillis = checkNotNull(retryDelay, "retryDelay is null").toMillis();
}
else {
this.retryDelayMillis = 0;
}
setTaps(taps);
}
@Override
public boolean processBatch(List<Event> entries)
{
List<URI> taps;
for (int i = 0; i <= retryCount; ++i) {
taps = this.taps.get();
if (sendEvents(taps, entries)) {
return true;
}
if (retryDelayMillis > 0) {
try {
sleep(retryDelayMillis);
}
catch (InterruptedException ignored) {
// Give up on this batch
Thread.currentThread().interrupt();
break;
}
}
}
onRecordsLost(entries.size());
return false;
}
@Override
public void notifyEntriesDropped(int count)
{
droppedEntries.getAndAdd(count);
eventCollectorStats.outboundEvents(eventType, flowId, DROPPED).add(count);
}
@Override
public Set<URI> getTaps()
{
return ImmutableSet.copyOf(taps.get());
}
@Override
public void setTaps(Set<URI> taps)
{
checkNotNull(taps, "taps is null");
checkArgument(!taps.isEmpty(), "taps is empty");
List<URI> existingTaps = this.taps.getAndSet(ImmutableList.copyOf(taps));
for (URI tap : taps) {
if (!existingTaps.contains(tap)) {
unestablishedTaps.add(tap);
}
}
}
private boolean sendEvents(List<URI> taps, List<Event> entries)
{
log.debug("About to send %s events to %s", entries.size(), taps);
Iterable<URI> randomizedTaps;
if (taps.size() > 1) {
int startPosition = RANDOM.nextInt(taps.size());
randomizedTaps = Iterables.concat(taps.subList(startPosition, taps.size()), taps.subList(0, startPosition));
}
else {
randomizedTaps = taps;
}
for (URI tap : randomizedTaps) {
try {
if (sendEvents(tap, entries)) {
return true;
}
}
catch (Exception ex) {
// failed, try the next one (if any).
log.warn(ex, "Error posting %s events to flow %s at %s ", eventType, flowId, tap);
}
}
return false;
}
private boolean sendEvents(final URI uri, final List<Event> entries)
throws Exception
{
Request.Builder requestBuilder = Request.builder().preparePost()
.setUri(uri)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.setBodySource(jsonBodyGenerator(eventsCodec, entries));
final boolean firstBatch = unestablishedTaps.remove(uri);
if (firstBatch) {
requestBuilder.addHeader(QOS_HEADER, QOS_HEADER_FIRST_BATCH);
}
// If there are multiple taps sharing the same flow that cares about
// dropped messages, they must coordinate what events were received
// anyway, so only one (the first) needs to get the dropped count.
final long count = droppedEntries.getAndSet(0);
if (count > 0) {
requestBuilder.addHeader(QOS_HEADER, format(QOS_HEADER_DROPPED_ENTRIES, count));
}
return httpClient.execute(requestBuilder.build(), new ResponseHandler<Boolean, Exception>()
{
@Override
public Boolean handleException(Request request, Exception exception)
throws Exception
{
restoreStateAfterError();
throw exception;
}
@Override
public Boolean handle(Request request, Response response)
{
if (fromStatusCode(response.getStatusCode()).getFamily() == SUCCESSFUL) {
log.debug("Posted %s events", entries.size());
onRecordsDelivered(uri, entries.size());
return true;
}
else if (fromStatusCode(response.getStatusCode()).getFamily() == CLIENT_ERROR) {
// Retrying will only result in the same error, give up completely.
log.error("Rejected %s events by flow %s at %s: response %d %s", eventType, flowId, uri, response.getStatusCode(), response.getStatusMessage());
restoreStateAfterError();
onRecordsRejected(uri, entries.size());
return true;
}
else {
log.warn("Error posting %s events to flow %s at %s: got response %s %s",
eventType, flowId, uri, response.getStatusCode(), response.getStatusMessage());
restoreStateAfterError();
return false;
}
}
private void restoreStateAfterError()
{
if (firstBatch) {
unestablishedTaps.add(uri);
}
droppedEntries.getAndAdd(count);
}
});
}
private void onRecordsDelivered(URI tap, int eventCount)
{
eventCollectorStats.outboundEvents(eventType, flowId, tap, DELIVERED).add(eventCount);
}
private void onRecordsLost(int eventCount)
{
droppedEntries.getAndAdd(eventCount);
eventCollectorStats.outboundEvents(eventType, flowId, LOST).add(eventCount);
}
private void onRecordsRejected(URI tap, int eventCount)
{
droppedEntries.getAndAdd(eventCount);
eventCollectorStats.outboundEvents(eventType, flowId, tap, REJECTED).add(eventCount);
}
@VisibleForTesting
String getEventType()
{
return eventType;
}
@VisibleForTesting
String getFlowId()
{
return flowId;
}
}