/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Gabriel Roldan (Boundless) - initial implementation */ package org.locationtech.geogig.rest; import java.util.ArrayList; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import org.locationtech.geogig.api.AbstractGeoGigOp; import org.locationtech.geogig.api.Context; import org.locationtech.geogig.api.DefaultProgressListener; import org.locationtech.geogig.api.GeogigTransaction; import com.google.common.base.Optional; import com.google.common.util.concurrent.ThreadFactoryBuilder; public class AsyncContext { public static final String CONTEXT_KEY = "GeoGigAsyncContext"; public static enum Status { WAITING, RUNNING, FINISHED, FAILED, CANCELLED } private static AsyncContext INSTANCE; public static synchronized AsyncContext get() { if (INSTANCE == null) { INSTANCE = new AsyncContext(); } return INSTANCE; } private Map<String, AsyncCommand<?>> commands = new ConcurrentHashMap<>(); private ScheduledExecutorService commandExecutor; private AsyncContext() { int nThreads = Math.max(2, Runtime.getRuntime().availableProcessors()); ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true) .setNameFormat("GeoGIG async tasks-%d").build(); this.commandExecutor = Executors.newScheduledThreadPool(nThreads, threadFactory); this.commandExecutor.scheduleAtFixedRate(new PruneTask(), 0, 10, TimeUnit.MINUTES); } private class PruneTask implements Runnable { @Override public void run() { Iterable<AsyncCommand<? extends Object>> all = AsyncContext.this.getAll(); for (AsyncCommand<?> c : all) { if (c.isDone()) { AsyncContext.this.commands.remove(c.getTaskId()); } } } } public <T> AsyncCommand<T> run(AbstractGeoGigOp<T> command, String description) { CommandCall<T> callable = new CommandCall<T>(command); Future<T> future = commandExecutor.submit(callable); AsyncCommand<T> asyncCommand = new AsyncCommand<T>(callable, future, description); commands.put(asyncCommand.getTaskId(), asyncCommand); return asyncCommand; } public Optional<AsyncCommand<?>> getAndPruneIfFinished(final String taskId) { Optional<AsyncCommand<?>> cmd = get(taskId); if (cmd.isPresent() && cmd.get().isDone()) { commands.remove(taskId); } return cmd; } public Optional<AsyncCommand<?>> get(final String taskId) { AsyncCommand<?> asyncCommand = commands.get(taskId); return Optional.<AsyncCommand<?>> fromNullable(asyncCommand); } public static class AsyncCommand<T> { private static AtomicLong ID_SEQ = new AtomicLong(); private final CommandCall<T> command; private final Future<T> future; private final String taskId; private String description; public AsyncCommand(CommandCall<T> command, Future<T> future, String description) { this.command = command; this.future = future; this.description = description; this.taskId = String.valueOf(ID_SEQ.incrementAndGet()); } public Optional<UUID> getTransactionId() { Context context = command.command.context(); if (context instanceof GeogigTransaction) { GeogigTransaction tx = (GeogigTransaction) context; UUID txId = tx.getTransactionId(); return Optional.of(txId); } return Optional.absent(); } public Status getStatus() { return command.status; } public String getStatusLine() { return command.progress.getDescription(); } public float getProgress() { return command.progress.getProgress(); } public boolean isDone() { return future.isDone(); } public T get() throws InterruptedException, ExecutionException { return future.get(); } public String getTaskId() { return taskId; } public String getDescription() { return description; } @SuppressWarnings("unchecked") public Class<? extends AbstractGeoGigOp<?>> getCommandClass() { return (Class<? extends AbstractGeoGigOp<?>>) command.commandClass; } public void tryCancel() { if (!isDone()) { command.command.getProgressListener().cancel(); } } } private static class CommandCall<T> implements Callable<T> { private final AbstractGeoGigOp<T> command; private final Class<?> commandClass; private Status status; private final DefaultProgressListener progress = new DefaultProgressListener(); public CommandCall(AbstractGeoGigOp<T> command) { this.command = command; this.commandClass = command.getClass(); this.status = Status.WAITING; } @Override public T call() throws Exception { if (command.getProgressListener().isCanceled()) { this.status = Status.CANCELLED; return null; } this.status = Status.RUNNING; try { command.setProgressListener(progress); T result = command.call(); if (command.getProgressListener().isCanceled()) { this.status = Status.CANCELLED; } else { this.status = Status.FINISHED; } return result; } catch (Throwable e) { this.status = Status.FAILED; throw e; } } } public Iterable<AsyncCommand<? extends Object>> getAll() { return new ArrayList<>(commands.values()); } }