package me.osm.gazetteer.web.executions;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import me.osm.gazetteer.web.GazetteerWeb;
import me.osm.gazetteer.web.api.meta.health.AbortedTaskError;
import me.osm.gazetteer.web.api.meta.health.BackgroundExecution;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.common.joda.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.util.LRUMap;
public class BackgroundExecutorFacade {
public static final int FUTURE_TASKS_QUEUE_SIZE = GazetteerWeb.config().getExecutionQueueSize();
private static final int EXECUTION_HISTORY_SIZE = 100;
public static abstract class BackgroundExecutableTask implements Runnable {
private final int id = taskCounter.getAndIncrement();
private final String uuid = UUID.randomUUID().toString();
private volatile boolean runed = false;
private volatile boolean aborted = false;
public int getId() {
return id;
}
public String getUUID() {
return uuid;
}
protected boolean isAborted() {
return aborted;
}
protected boolean isRuned() {
return runed;
}
public void abort() {
aborted = true;
}
public String getCallbackURL() {
return null;
}
@Override
public final void run() {
runed = true;
synchronized (INSTANCE) {
INSTANCE.activeTasks.add(this.id);
BackgroudTaskDescription dsc = INSTANCE.descriptions.get(this.id);
if(dsc != null) {
dsc.setRunTs(LocalDateTime.now());
}
}
try{
executeTask();
synchronized (INSTANCE) {
INSTANCE.doneTasks.add(this.id);
BackgroudTaskDescription dsc = INSTANCE.descriptions.get(this.id);
if(dsc != null) {
dsc.setDoneTs(LocalDateTime.now());
}
}
log.info("Task {} is done", this.id);
if(StringUtils.isNotBlank(getCallbackURL())) {
callBack(getCallbackURL(), null);
}
}
catch (AbortedException abortedE) {
synchronized (INSTANCE) {
BackgroudTaskDescription dsc = INSTANCE.descriptions.get(this.id);
if(dsc != null) {
dsc.setDoneTs(LocalDateTime.now());
}
INSTANCE.abortedTasks.put(this.id, abortedE.isByUser() ? "Aborted by user" : abortedE.getMessage());
}
if(StringUtils.isNotBlank(getCallbackURL())) {
callBack(getCallbackURL(), abortedE);
}
if(!abortedE.isByUser()) {
abortedE.printStackTrace();
throw new RuntimeException(abortedE.getCause());
}
}
finally {
INSTANCE.queuedTasks.remove((Object)this.id);
INSTANCE.activeTasks.remove(this.id);
INSTANCE.cleanupHistory();
}
}
private void callBack(String callbackURL, AbortedException abortedE) {
String status = "done";
String errorMsg = "";
if (abortedE != null) {
status = abortedE.isByUser() ? "aborted_by_user" : "error";
errorMsg = abortedE.isByUser() ? "Aborted by user" : abortedE.getMessage();
}
callbackURL = StringUtils.replace(callbackURL, "{task_id}", String.valueOf(this.id));
callbackURL = StringUtils.replace(callbackURL, "{task_uuid}", this.getUUID());
callbackURL = StringUtils.replace(callbackURL, "{status}", status);
try {
callbackURL = StringUtils.replace(callbackURL, "{error_msg}", URLEncoder.encode(errorMsg, "utf-8"));
}
catch (Exception e) {
callbackURL = StringUtils.replace(callbackURL, "{error_msg}", "");
log.error("Failed to url encode error_msg callback parameter.", e);
}
try {
URLConnection connection = new URL(callbackURL).openConnection();
// 5 Seconds timeout
connection.setReadTimeout(5 * 1000);
InputStream is = connection.getInputStream();
is.close();
log.info("Call {}", callbackURL);
}
catch (Exception e) {
log.error("Callback {} invocation failed.", callbackURL, e);
}
}
public boolean submit() {
synchronized (INSTANCE) {
if(INSTANCE.queuedTasks.size() > FUTURE_TASKS_QUEUE_SIZE) {
return false;
}
INSTANCE.queuedTasks.add(this.id);
BackgroudTaskDescription description = this.description();
description.setSubmitTs(LocalDateTime.now());
INSTANCE.descriptions.put(this.id, description);
}
executor.submit(this);
return true;
}
public abstract void executeTask() throws AbortedException;
public abstract BackgroudTaskDescription description();
}
private static final AtomicInteger taskCounter = new AtomicInteger();
private static ExecutorService executor = Executors.newSingleThreadExecutor();
private final Set<Integer> doneTasks = Collections.synchronizedSet(new LinkedHashSet<Integer>(EXECUTION_HISTORY_SIZE + 2));
private final Set<Integer> activeTasks = Collections.synchronizedSet(new LinkedHashSet<Integer>(10));
private final LinkedHashMap<Integer, String> abortedTasks = new LinkedHashMap<Integer, String>(EXECUTION_HISTORY_SIZE + 2);
private final List<Integer> queuedTasks = new ArrayList<Integer>();
private final Map<Integer, BackgroudTaskDescription> descriptions
= new LRUMap<>(EXECUTION_HISTORY_SIZE + 2 + 10, EXECUTION_HISTORY_SIZE + 2 + 10 + 5);
private static final Logger log = LoggerFactory.getLogger(BackgroundExecutorFacade.class);
private BackgroundExecutorFacade() {
}
public synchronized void cleanupHistory() {
if(doneTasks.size() > EXECUTION_HISTORY_SIZE) {
int overhead = doneTasks.size() - EXECUTION_HISTORY_SIZE;
Iterator<Integer> iterator = doneTasks.iterator();
while(overhead-- > 0) {
log.info("Remove {} task from DONE tasks execution history", iterator.next());
iterator.remove();
}
}
if(abortedTasks.size() > EXECUTION_HISTORY_SIZE) {
int overhead = abortedTasks.size() - EXECUTION_HISTORY_SIZE;
Iterator<Entry<Integer, String>> iterator = abortedTasks.entrySet().iterator();
while(overhead-- > 0) {
Entry<Integer, String> entry = iterator.next();
log.info("Remove [{}: {}] task from ABORTED tasks execution history", entry.getKey(), entry.getValue());
iterator.remove();
}
}
}
private static final BackgroundExecutorFacade INSTANCE = new BackgroundExecutorFacade();
public static BackgroundExecutorFacade get() {
return INSTANCE;
}
public BackgroundExecution getStateInfo() {
BackgroundExecution result = new BackgroundExecution();
result.setThreads(1);
synchronized (INSTANCE) {
List<BackgroudTaskDescription> list = new ArrayList<>();
for(int tid : activeTasks) {
list.add(INSTANCE.descriptions.get(tid));
}
result.setActive(list);
list = new ArrayList<>();
for(int tid : doneTasks) {
list.add(INSTANCE.descriptions.get(tid));
}
result.setDone(list);
ArrayList<Integer> queued = new ArrayList<Integer>(queuedTasks);
for(int tid : activeTasks) {
int indexOf = queued.indexOf(tid);
if(indexOf >= 0) {
queued.remove(indexOf);
}
}
list = new ArrayList<>();
for(int tid : queued) {
list.add(INSTANCE.descriptions.get(tid));
}
result.setQueued(list);
Collection<AbortedTaskError> rejected = new ArrayList<>();
for(Entry<Integer, String> entry : abortedTasks.entrySet()) {
BackgroudTaskDescription description = INSTANCE.descriptions.get(entry.getKey());
String errMsg = entry.getValue();
rejected.add(new AbortedTaskError(description, errMsg));
}
result.setAborted(rejected);
return result;
}
}
}