/* Copyright 2014 TellApart, 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.tellapart.taba.engine; import com.tellapart.taba.TabaClientProperties; import com.tellapart.taba.event.Event; import com.tellapart.taba.event.EventPayload; import com.tellapart.taba.Transport; import org.apache.http.annotation.GuardedBy; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * Default implementation of the TabaClientEngine. Buffers Events in memory and * uses a background Thread to flush the buffer to a remote HTTP Agent or * Server. */ public class DefaultClientEngine implements TabaClientEngine { private final Logger logger = LoggerFactory.getLogger(DefaultClientEngine.class); private final String clientId; private final long flushPeriod; private final String eventPostUrl; private CloseableHttpClient httpClient; private final ScheduledExecutorService flushScheduler; private ScheduledFuture<?> flusherHandle; private final Object lock = new Object(); private @GuardedBy("lock") Map<String, List<Event>> buffer; private long bufferSize; @Inject public DefaultClientEngine( TabaClientProperties properties, CloseableHttpClient client, ScheduledExecutorService executor ) { this.clientId = properties.getClientId(); this.flushPeriod = properties.getFlushPeriodMillis(); this.eventPostUrl = properties.getPostUrl(); httpClient = client; flushScheduler = executor; // Buffer to hold events until they are flushed. buffer = new HashMap<>(); bufferSize = 0; } @Override public synchronized void start() { if(flusherHandle != null) { throw new IllegalStateException("Cannot call start multiple times."); } final Runnable flusher = new Runnable() { @Override public void run() { flush(); } }; flusherHandle = flushScheduler.scheduleAtFixedRate( flusher, flushPeriod, flushPeriod, TimeUnit.MILLISECONDS); } @Override public void stop() { // Cancel the scheduled runnables. flusherHandle.cancel(false); // Disable new tasks from being submitted and clean up the scheduler; flushScheduler.shutdown(); try { // Wait for existing tasks to terminate. if (!flushScheduler.awaitTermination(flushPeriod, TimeUnit.SECONDS)) { flushScheduler.shutdownNow(); // Wait for tasks to respond to being canceled. if (!flushScheduler.awaitTermination(flushPeriod, TimeUnit.SECONDS)) { logger.error("Taba Client flush scheduler did not terminate."); } } // Process any leftover buffer synchronously. flush(); } catch (InterruptedException e) { // Re-cancel if current thread also interrupted. flushScheduler.shutdownNow(); // Preserve interrupt status. Thread.currentThread().interrupt(); } // Close any HTTP sessions. try { httpClient.close(); } catch(IOException e) { logger.error("Error stopping Taba Client HTTP Client"); } } @Override public void recordEvent(String name, String type, EventPayload payload) { // Lock while we record the event into the buffer. synchronized (lock) { if (!buffer.containsKey(name)) { buffer.put(name, new ArrayList<Event>()); } buffer.get(name).add(new Event(type, System.currentTimeMillis() / 1000, payload)); bufferSize += 1; } } /** * Flush all buffered Events to the remote end-point. */ protected void flush() { if (bufferSize == 0) { return; } // Clear the existing buffer. Map<String, List<Event>> flushBuffer; synchronized (lock) { flushBuffer = buffer; buffer = new HashMap<>(); bufferSize = 0; } String body = Transport.encode(clientId, flushBuffer); HttpPost httpPost = new HttpPost(eventPostUrl); try { StringEntity entity; entity = new StringEntity(body); entity.setContentType("application/json"); httpPost.setEntity(entity); try (CloseableHttpResponse response = httpClient.execute(httpPost)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != 200) { logger.error(String.format("Bad status code: %d", statusCode)); } } catch (IOException e) { logger.error("Error flushing Taba Client buffer", e); } } catch (UnsupportedEncodingException e) { logger.error("Error encoding Taba Client buffer", e); } } }