package org.gbif.ipt.config;
import org.gbif.ipt.action.BaseAction;
import org.gbif.ipt.model.Resource;
import org.gbif.ipt.service.InvalidConfigException;
import org.gbif.ipt.service.PublicationException;
import org.gbif.ipt.service.admin.RegistrationManager;
import org.gbif.ipt.service.manage.ResourceManager;
import org.gbif.ipt.struts2.SimpleTextProvider;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.log4j.Logger;
/**
* Class used to start a monitor thread which is responsible for auto-publishing resources when they are due,
* and which ensures publication always finishes entirely.
*/
@Singleton
public class PublishingMonitor {
// 10 second interval
public static final int MONITOR_INTERVAL_MS = 10000;
private static Thread monitorThread;
private static final Logger LOG = Logger.getLogger(PublishingMonitor.class);
private AtomicBoolean running;
private final ResourceManager resourceManager;
private final BaseAction baseAction;
@Inject
public PublishingMonitor(SimpleTextProvider textProvider, AppConfig cfg, RegistrationManager registrationManager,
ResourceManager resourceManager) {
this.resourceManager = resourceManager;
baseAction = new BaseAction(textProvider, cfg, registrationManager);
}
/**
* Polls the queue and launches threads if possible.
*/
@Singleton
class QueueMonitor implements Runnable {
private ResourceManager resourceManager;
public QueueMonitor(ResourceManager resourceManager) {
this.resourceManager = resourceManager;
running = new AtomicBoolean();
}
public void run() {
running.set(true);
while (running.get()) {
try {
// monitor resources that are currently being published or have finished
Map<String, Future<Map<String, Integer>>> processFutures = resourceManager.getProcessFutures();
Set<String> shortNames = new HashSet<String>();
if (!processFutures.isEmpty()) {
// copy futures into new set, to avoid concurrent modification exception
shortNames.addAll(processFutures.keySet());
// in order for publishing to finish entirely, resourceManager.isLocked() must be called
for (String shortName : shortNames) {
resourceManager.isLocked(shortName, baseAction);
}
}
// might as well check if we can handle more publishing jobs
ThreadPoolExecutor executor = resourceManager.getExecutor();
if (executor.getMaximumPoolSize() - executor.getActiveCount() > 0) {
Date now = new Date();
List<Resource> resources = resourceManager.list();
for (Resource resource : resources) {
Date next = resource.getNextPublished();
BigDecimal nextVersion = new BigDecimal(resource.getNextVersion().toPlainString());
if (next != null) {
// ensure resource is due to be auto-published
if (next.before(now)) {
// ensure resource isn't already being published
if (!shortNames.contains(resource.getShortname())) {
// ensure resource has not exceeded the maximum number of publication failures
if (!resourceManager.hasMaxProcessFailures(resource)) {
try {
LOG.debug("Monitor: " + resource.getTitleAndShortname() + " v# " + nextVersion.toPlainString()
+ " due to be auto-published: " + next.toString());
resourceManager.publish(resource, nextVersion, null);
} catch (PublicationException e) {
if (PublicationException.TYPE.LOCKED == e.getType()) {
LOG.error("Monitor: " + resource.getTitleAndShortname()
+ " cannot be auto-published, because "
+ "it is currently being published");
} else {
// alert user publication failed
LOG.error(
"Publishing version #" + nextVersion.toPlainString() + " of resource "
+ resource.getTitleAndShortname()
+ " failed: " + e.getMessage());
// restore the previous version since publication was unsuccessful
resourceManager.restoreVersion(resource, nextVersion, null);
// keep track of how many failures on auto publication have happened
resourceManager.getProcessFailures().put(resource.getShortname(), new Date());
}
} catch (InvalidConfigException e) {
// with this type of error, the version cannot be rolled back - just alert user it failed
LOG.error(
"Publishing version #" + nextVersion.toPlainString() + "of resource " + resource.getShortname()
+ "failed:" + e.getMessage(), e);
}
} else {
LOG.debug("Skipping auto-publication for [" + resource.getTitleAndShortname()
+ "] since it has exceeded the maximum number of failed publish attempts. Please try "
+ "to publish this resource individually to fix the problem(s)");
}
} else {
LOG.debug("Skipping auto-publication for [" + resource.getTitleAndShortname()
+ "] since it is already in progress");
}
}
}
}
}
// just poll once every 'interval' milliseconds
Thread.sleep(MONITOR_INTERVAL_MS);
} catch (InterruptedException e) {
// should the thread have been interrupted, encountered when trying to sleep
LOG.error("Monitor thread has been interrupted!", e);
}
}
}
}
/**
* Start the publishing monitor thread itself.
*/
private void startMonitorThread() {
monitorThread = new Thread(new QueueMonitor(resourceManager));
monitorThread.start();
LOG.debug("The monitor thread has started.");
}
/**
* Starts the publishing monitor once and only once.
*/
public void start() {
if (monitorThread != null) {
if (!monitorThread.isAlive()) {
startMonitorThread();
} else {
LOG.error("The monitor thread is already running");
}
} else {
startMonitorThread();
}
}
/**
* Stops the publishing monitor.
*/
public void stop() {
running.set(false);
monitorThread = null;
}
}