/**
* Analytica - beta version - Systems Monitoring Tool
*
* Copyright (C) 2013, KleeGroup, direction.technique@kleegroup.com (http://www.kleegroup.com)
* KleeGroup, Centre d'affaire la Boursidi�re - BP 159 - 92357 Le Plessis Robinson Cedex - France
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, see <http://www.gnu.org/licenses>
*
* Linking this library statically or dynamically with other modules is making a combined work based on this library.
* Thus, the terms and conditions of the GNU General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you permission to link this library
* with independent modules to produce an executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the license of that module.
* An independent module is a module which is not derived from or based on this library.
* If you modify this library, you may extend this exception to your version of the library,
* but you are not obliged to do so.
* If you do not wish to do so, delete this exception statement from your version.
*/
package io.analytica.agent.impl.net;
import io.analytica.KProcessJsonCodec;
import io.analytica.api.KProcess;
import io.analytica.api.KProcessConnector;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status.Family;
import net.sf.ehcache.Element;
import org.apache.log4j.Logger;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.api.client.WebResource;
/**
* TODO voir http://ghads.wordpress.com/2008/09/24/calling-a-rest-webservice-from-java-without-libs/
* @author npiedeloup
* @version $Id: RemoteNetPlugin.java,v 1.4 2012/06/14 13:49:17 npiedeloup Exp $
*/
public final class RemoteConnector implements KProcessConnector {
private static final String SPOOL_CONTEXT = "Analytica_Spool";
private static final String VERSION_MAJOR = "1.0"; //definit la compatibilit�
private static final String VERSION_MINOR = "0";
private static final String VERSION = VERSION_MAJOR + "." + VERSION_MINOR;
private final Logger logger = Logger.getLogger(RemoteConnector.class);
private Thread processSenderThread = null;
private final ConcurrentLinkedQueue<KProcess> processQueue = new ConcurrentLinkedQueue<KProcess>();
private net.sf.ehcache.CacheManager manager;
private final String serverUrl;
private final int sendPaquetSize;
private final int sizeCheckFrequencyMs;
private final int sendPaquetFrequencySeconds;
private Client locatorClient;
private WebResource remoteWebResource;
private final int maxResendJson = 5; //On limite a 5 car ce sont d�j� des paquets de sendPaquetSize Processes
/**
* @param serverUrl Url du serveur Analytica
* @param sendPaquetSize Taille des paquets d�clenchant l'envoi anticip�
* @param sendPaquetFrequencySeconds Frequence normal d'envoi des paquets (en seconde)
*/
public RemoteConnector(final String serverUrl, final int sendPaquetSize, final int sendPaquetFrequencySeconds) {
this.serverUrl = serverUrl;
this.sendPaquetSize = sendPaquetSize;
this.sendPaquetFrequencySeconds = sendPaquetFrequencySeconds;
sizeCheckFrequencyMs = 250;
}
/** {@inheritDoc} */
public void add(final KProcess process) {
processQueue.add(process);
// if (processQueue.size() >= sendPaquetSize) {
// synchronized (processQueue) {
// processQueue.notify();
// logger.trace("processQueue.size() >= sendPaquetSize notify : " + processQueue.size() + " >= " + sendPaquetSize);
// }
// }
}
/** {@inheritDoc} */
public void start() {
locatorClient = Client.create();
locatorClient.addFilter(new com.sun.jersey.api.client.filter.GZIPContentEncodingFilter());
remoteWebResource = locatorClient.resource(serverUrl);
manager = net.sf.ehcache.CacheManager.create();
if (!manager.cacheExists(SPOOL_CONTEXT)) {
final boolean overflowToDisk = true;
final boolean eternal = false;
final int timeToLiveSeconds = 8 * 60 * 60; //on accept 8h d'indisponibilit� max
final int timeToIdleSeconds = timeToLiveSeconds;
final int maxElementsInMemory = 1;//0 = illimit�, 1 car on souhaite le minimum d'empreinte m�moire
final net.sf.ehcache.Cache cache = new net.sf.ehcache.Cache(SPOOL_CONTEXT, maxElementsInMemory, overflowToDisk, eternal, timeToLiveSeconds, timeToIdleSeconds);
manager.addCache(cache);
}
processSenderThread = new SendProcessThread(this);
processSenderThread.start();
checkServerVersion();
logger.info("Start Analytica RemoteNetPlugin : connect to " + serverUrl);
}
/** {@inheritDoc} */
public void stop() {
logger.info("Stopping Analytica RemoteNetPlugin");
processSenderThread.interrupt();
try {
processSenderThread.join(10000);//on attend 10s max
} catch (final InterruptedException e) {
//rien, si interrupt on continu l'arret
}
processSenderThread = null;
flushAllProcessQueue();
locatorClient = null;
remoteWebResource = null;
manager.shutdown();
logger.info("Stop Analytica RemoteNetPlugin");
}
private static class SendProcessThread extends Thread {
private final RemoteConnector remoteConnector;
//private final Logger logger = Logger.getLogger(SendProcessThread.class);
SendProcessThread(final RemoteConnector remoteConnector) {
super("AnalyticaSendProcessThread");
setDaemon(false); //ce n'est pas un d�mon car on veux envoyer les derniers process
if (remoteConnector == null) {
throw new NullPointerException("remoteConnector is required");
}
//-----------------------------------------------------------------
this.remoteConnector = remoteConnector;
}
/** {@inheritDoc} */
@Override
public void run() {
while (!isInterrupted()) {
//System.out.println("WHILE processSenderThread isInterrupted() :" + isInterrupted());
try {
remoteConnector.waitToSendPacket();
} catch (final InterruptedException e) {
interrupt();//On remet le flag qui a �t� reset lors du throw InterruptedException (pour le test isInterrupted())
//logger.trace("interrupt()");
//on envoi avant l'arret du serveur
}
//On flush la queue sur :
// - le timeout
// - un processQueue.notify (taille max de la queue atteinte)
// - un interrupt (arret du serveur)
remoteConnector.retrySendProcesses();
remoteConnector.flushProcessQueue();
//System.out.println("WHILE processSenderThread isInterrupted() :" + isInterrupted());
}
//System.out.println("END processSenderThread isInterrupted() :" + isInterrupted());
}
}
/**
* On attend la constitution d'un paquet.
* Rend la main apr�s :
* - le timeout
* - un processQueue.notify (taille max de la queue atteinte)
* - un interrupt (arret du serveur)
* @throws InterruptedException Si interrupt
*/
void waitToSendPacket() throws InterruptedException {
final long start = System.currentTimeMillis();
while (processQueue.size() < sendPaquetSize //
&& System.currentTimeMillis() - start < sendPaquetFrequencySeconds * 1000) {
Thread.sleep(sizeCheckFrequencyMs);
}
// synchronized (processQueue) { //synchronized pour recevoir le notifiy
// logger.trace("processQueue.wait");
// processQueue.wait(sendPaquetFrequencySeconds * 1000);
// logger.trace("processQueue.wait continue");
// }
}
/**
* Flush la queue des process (au max sendPaquetSize * 2).
*/
void flushProcessQueue() {
flushProcessQueue(sendPaquetSize * 2); //si besoin on va jusqu'a un sur-booking x2 des paquets
}
/**
* Flush toute la queue des processes (utilis�e lors de l'arret de l'agent)
*/
void flushAllProcessQueue() {
flushProcessQueue(Long.MAX_VALUE);
}
/**
* Effectue le flush de la queue des processes � envoyer.
*/
private void flushProcessQueue(final long maxPaquetSize) {
long sendPaquet = 0;
final List<KProcess> processes = new ArrayList<KProcess>();
KProcess head;
do {
head = processQueue.poll();
if (head != null) {
processes.add(head);
}
if (processes.size() >= sendPaquetSize * 2) { //si besoin on va jusqu'a un sur-booking x2 des paquets
doSendProcesses(processes);
sendPaquet += processes.size();
processes.clear();
}
} while (head != null && sendPaquet < maxPaquetSize);
doSendProcesses(processes);
//On n'utilise pas le MediaType.APPLICATION_JSON, car jackson a besoin de modifications sur KProcess
//final ClientResponse response = remoteWeResource.accept(MediaType.APPLICATION_JSON).put(ClientResponse.class, processes);
}
private void doSendProcesses(final List<KProcess> processes) {
if (!processes.isEmpty()) {
final String json = KProcessJsonCodec.toJson(processes);
try {
doSendJson(remoteWebResource, json);
logger.info("Send " + processes.size() + " processes to " + serverUrl + "(" + processQueue.size() + " remaining)");
} catch (final Exception e) {
logSendError(false, e);
doStoreJson(json);
}
}
}
/**
* Tente de renvoyer les paquets qui ont �chou�s.
*/
void retrySendProcesses() {
try {
final List<UUID> keys = manager.getCache(SPOOL_CONTEXT).getKeys();
for (int i = 0; i < maxResendJson && i < keys.size(); i++) { //on limite a 5 car ce sont d�j� des paquets constitu�s
final UUID key = keys.get(i);
doSendJson(remoteWebResource, (String) manager.getCache(SPOOL_CONTEXT).get(key).getValue());
manager.getCache(SPOOL_CONTEXT).remove(key); //si l'envoi est pass�, on retire du cache
}
} catch (final Exception e) {
//serveur indisponible ou en erreur
logSendError(true, e);
}
}
private void logSendError(final boolean isResend, final Exception e) {
//serveur indisponible ou en erreur
final String action = isResend ? "Resend" : "Send";
if (logger.isDebugEnabled()) {
logger.debug(action + " : Serveur Analytica indisponible : " + e.getMessage(), e);
} else {
final String message = action + " : Serveur Analytica indisponible : " + e.getMessage();
//volontairement on ne passe pas l'exception pour ne pas saturer le log
if (isResend) {
logger.info(message);
} else {
logger.warn(message);
}
}
}
private void doStoreJson(final String json) {
final Element element = new Element(UUID.randomUUID(), json);
manager.getCache(SPOOL_CONTEXT).put(element);
}
private void doSendJson(final WebResource webResource, final String json) {
final ClientResponse response = webResource.accept(MediaType.TEXT_PLAIN).post(ClientResponse.class, json);
checkResponseStatus(response);
}
private String doGet(final WebResource webResource) {
final ClientResponse response = webResource.get(ClientResponse.class);
checkResponseStatus(response);
return response.getEntity(String.class);
}
private static void checkResponseStatus(final ClientResponse response) {
final Status status = response.getClientResponseStatus();
if (status.getFamily() == Family.SUCCESSFUL) {
return;
}
throw new RuntimeException("Une erreur est survenue : " + status.getStatusCode() + " " + status.getReasonPhrase());
}
private void checkServerVersion() {
//On check la version
final WebResource remoteVersionWebResource = locatorClient.resource(serverUrl + "/version");
try {
final String serverVersion = doGet(remoteVersionWebResource);
if (!serverVersion.startsWith(VERSION_MAJOR)) {
logger.warn("Cette version du client Analytica (" + VERSION + ") n''est pas compatible avec la version du serveur (" + serverVersion + ")");
} else {
logger.info("Connexion OK avec le serveur Analytica (" + serverUrl + ")");
}
} catch (final Exception e) {
//serveur indisponible ou en erreur
logger.warn("Serveur Analytica indisponible (" + serverUrl + ") : " + e.getMessage());
}
}
}