/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.usergrid.services.queues;
import com.codahale.metrics.*;
import com.codahale.metrics.Timer;
import com.google.inject.Injector;
import org.apache.usergrid.persistence.EntityManagerFactory;
import org.apache.usergrid.persistence.core.metrics.MetricsFactory;
import org.apache.usergrid.persistence.queue.*;
import org.apache.usergrid.persistence.queue.LegacyQueueManager;
import org.apache.usergrid.persistence.queue.impl.LegacyQueueScopeImpl;
import org.apache.usergrid.services.ServiceManager;
import org.apache.usergrid.services.ServiceManagerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Listens to the SQS queue and polls it for more queue messages. Then hands the queue messages off to the
* QueueProcessorFactory
*/
public abstract class QueueListener {
public final int MESSAGE_TRANSACTION_TIMEOUT = 25 * 1000;
private final LegacyQueueManagerFactory queueManagerFactory;
public long DEFAULT_SLEEP = 5000;
private static final Logger logger = LoggerFactory.getLogger(QueueListener.class);
private MetricsFactory metricsService;
private ServiceManagerFactory smf;
private EntityManagerFactory emf;
private Properties properties;
private ServiceManager svcMgr;
private long sleepWhenNoneFound = 0;
private long sleepBetweenRuns = 0;
private ExecutorService pool;
private List<Future> futures;
public final int MAX_THREADS = 2;
private Integer batchSize = 10;
private String queueName;
private int consecutiveCallsToRemoveDevices;
private Meter meter;
private Timer timer;
/**
* Initializes the QueueListener.
* @param smf
* @param emf
* @param props
*/
public QueueListener(ServiceManagerFactory smf, EntityManagerFactory emf, Injector injector, Properties props){
//TODO: change current injectors to use service module instead of CpSetup
this.queueManagerFactory = injector.getInstance( LegacyQueueManagerFactory.class );
this.smf = smf;
this.emf = injector.getInstance( EntityManagerFactory.class ); //emf;
this.metricsService = injector.getInstance(MetricsFactory.class);
this.properties = props;
meter = metricsService.getMeter(QueueListener.class, "execute.commit");
timer = metricsService.getTimer(QueueListener.class, "execute.dequeue");
}
/**
* Start the queueListener. Initializes queue settings before starting the queue.
*/ //TODO: make use guice. Currently on spring. Needs to run and finish for main thread.
@PostConstruct
public void start(){
boolean shouldRun = new Boolean(properties.getProperty("usergrid.queues.listener.run", "true"));
if(shouldRun) {
if (logger.isTraceEnabled()) {
logger.trace("QueueListener: starting.");
}
int threadCount = 0;
try {
sleepBetweenRuns = new Long(properties.getProperty("usergrid.queues.listener.sleep.between", ""+sleepBetweenRuns)).longValue();
sleepWhenNoneFound = new Long(properties.getProperty("usergrid.queues.listener.sleep.after", ""+DEFAULT_SLEEP)).longValue();
batchSize = new Integer(properties.getProperty("usergrid.queues.listener.MAX_TAKE", (""+batchSize)));
consecutiveCallsToRemoveDevices = new Integer(properties.getProperty("usergrid.queues.inactive.interval", ""+200));
queueName = getQueueName();
int maxThreads = new Integer(properties.getProperty("usergrid.queues.listener.maxThreads", ""+MAX_THREADS));
futures = new ArrayList<Future>(maxThreads);
//create our thread pool based on our threadcount.
pool = Executors.newFixedThreadPool(maxThreads);
while (threadCount++ < maxThreads) {
if (logger.isTraceEnabled()) {
logger.trace("QueueListener: Starting thread {}.", threadCount);
}
Runnable task = new Runnable() {
@Override
public void run() {
try {
execute();
} catch (Exception e) {
logger.warn("failed to start push", e);
}
}
};
futures.add( pool.submit(task));
}
} catch (Exception e) {
logger.error("QueueListener: failed to start:", e);
}
if (logger.isTraceEnabled()) {
logger.trace("QueueListener: done starting.");
}
}else{
logger.info("QueueListener: never started due to config value usergrid.queues.listener.run.");
}
}
/**
* Queue Processor
* Polls the queue for messages, then processes the messages that it gets.
*/
private void execute(){
if(Thread.currentThread().isDaemon()) {
Thread.currentThread().setDaemon(true);
}
Thread.currentThread().setName("queues_Processor"+UUID.randomUUID());
final AtomicInteger consecutiveExceptions = new AtomicInteger();
if (logger.isTraceEnabled()) {
logger.trace("QueueListener: Starting execute process.");
}
svcMgr = smf.getServiceManager(smf.getManagementAppId());
if (logger.isTraceEnabled()) {
logger.trace("getting from queue {} ", queueName);
}
LegacyQueueScope queueScope = new LegacyQueueScopeImpl( queueName, LegacyQueueScope.RegionImplementation.LOCAL);
LegacyQueueManager legacyQueueManager = queueManagerFactory.getQueueManager(queueScope);
// run until there are no more active jobs
long runCount = 0;
while ( true ) {
Timer.Context timerContext = timer.time();
//Get the messages out of the queue.
//TODO: a model class to get generic queueMessages out of the queueManager. Ask Shawn what should go here.
rx.Observable.from( legacyQueueManager.getMessages(getBatchSize(), ImportQueueMessage.class))
.buffer(getBatchSize())
.doOnNext(messages -> {
try {
if (logger.isTraceEnabled()) {
logger.trace("retrieved batch of {} messages from queue {} ", messages.size(), queueName);
}
if (messages.size() > 0) {
long now = System.currentTimeMillis();
//TODO: make sure this has a way to determine which QueueListener needs to be used
// ideally this is done by checking which type the messages have then
// asking for a onMessage call.
onMessage(messages);
legacyQueueManager.commitMessages(messages);
meter.mark(messages.size());
if (logger.isTraceEnabled()) {
logger.trace("sent batch {} messages duration {} ms", messages.size(), System.currentTimeMillis() - now);
}
if (sleepBetweenRuns > 0) {
if (logger.isTraceEnabled()) {
logger.trace("sleep between rounds...sleep...{}", sleepBetweenRuns);
}
Thread.sleep(sleepBetweenRuns);
}
} else {
if (logger.isTraceEnabled()) {
logger.trace("no messages...sleep...{}", sleepWhenNoneFound);
}
Thread.sleep(sleepWhenNoneFound);
}
timerContext.stop();
//send to the providers
consecutiveExceptions.set(0);
} catch (Exception ex) {
logger.error("failed to dequeue", ex);
try {
long sleeptime = sleepWhenNoneFound * consecutiveExceptions.incrementAndGet();
long maxSleep = 15000;
sleeptime = sleeptime > maxSleep ? maxSleep : sleeptime;
if (logger.isTraceEnabled()) {
logger.trace("sleeping due to failures {} ms", sleeptime);
}
Thread.sleep(sleeptime);
} catch (InterruptedException ie) {
if (logger.isTraceEnabled()) {
logger.trace("sleep interrupted");
}
}
}
}).toBlocking().lastOrDefault(null);
}
}
public void stop(){
if (logger.isTraceEnabled()) {
logger.trace("stop processes");
}
if(futures == null){
return;
}
for(Future future : futures){
future.cancel(true);
}
pool.shutdownNow();
}
public void setBatchSize(int batchSize){
this.batchSize = batchSize;
}
public int getBatchSize(){return batchSize;}
/**
* This will be the method that does the job dependant execution.
* @param messages
*/
public abstract void onMessage(List<LegacyQueueMessage> messages) throws Exception;
public abstract String getQueueName();
}