package com.airbnb.airpal.core.execution;
import com.airbnb.airpal.api.ExecutionRequest;
import com.airbnb.airpal.api.Job;
import com.airbnb.airpal.api.JobState;
import com.airbnb.airpal.api.event.JobFinishedEvent;
import com.airbnb.airpal.api.output.HiveTablePersistentOutput;
import com.airbnb.airpal.api.output.PersistentJobOutputFactory;
import com.airbnb.airpal.api.output.builders.OutputBuilderFactory;
import com.airbnb.airpal.api.output.persistors.PersistorFactory;
import com.airbnb.airpal.core.AirpalUser;
import com.airbnb.airpal.core.store.history.JobHistoryStore;
import com.airbnb.airpal.core.store.jobs.ActiveJobsStore;
import com.airbnb.airpal.core.store.usage.UsageStore;
import com.airbnb.airpal.presto.QueryInfoClient;
import com.airbnb.airpal.presto.Table;
import com.airbnb.airpal.presto.metadata.ColumnCache;
import com.airbnb.airpal.presto.metadata.SchemaCache;
import com.facebook.presto.client.Column;
import com.facebook.presto.client.QueryError;
import com.google.common.eventbus.EventBus;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.inject.Inject;
import lombok.Getter;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import static com.airbnb.airpal.presto.QueryRunner.QueryRunnerFactory;
public class ExecutionClient
{
private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(
Executors.newCachedThreadPool(SchemaCache.daemonThreadsNamed("execution-client-%d")));
@Getter
private final EventBus eventBus;
@Getter
private final JobHistoryStore historyStore;
private final PersistentJobOutputFactory persistentJobOutputFactory;
@Getter
private final UsageStore usageStore;
@Getter
private final SchemaCache schemaCache;
private final ColumnCache columnCache;
private final QueryInfoClient queryInfoClient;
private final QueryRunnerFactory queryRunnerFactory;
private final ActiveJobsStore activeJobsStore;
private final OutputBuilderFactory outputBuilderFactory;
private final PersistorFactory persistorFactory;
private final Map<UUID, Execution> executionMap = new ConcurrentHashMap<>();
@Inject
public ExecutionClient(QueryRunnerFactory queryRunnerFactory,
EventBus eventBus,
JobHistoryStore historyStore,
PersistentJobOutputFactory persistentJobOutputFactory,
UsageStore usageStore,
SchemaCache schemaCache,
ColumnCache columnCache,
QueryInfoClient queryInfoClient,
ActiveJobsStore activeJobsStore,
OutputBuilderFactory outputBuilderFactory,
PersistorFactory persistorFactory)
{
this.queryRunnerFactory = queryRunnerFactory;
this.eventBus = eventBus;
this.historyStore = historyStore;
this.persistentJobOutputFactory = persistentJobOutputFactory;
this.usageStore = usageStore;
this.schemaCache = schemaCache;
this.columnCache = columnCache;
this.queryInfoClient = queryInfoClient;
this.activeJobsStore = activeJobsStore;
this.outputBuilderFactory = outputBuilderFactory;
this.persistorFactory = persistorFactory;
}
public UUID runQuery(final ExecutionRequest request,
final AirpalUser user,
final String schema,
final Duration timeout)
{
return runQuery(request.getQuery(), request.getTmpTable(), user, schema, timeout);
}
public UUID runQuery(final String query,
final String tmpTable,
final AirpalUser user,
final String schema,
final Duration timeout)
{
final UUID uuid = UUID.randomUUID();
final Job job = new Job(user.getUserName(),
query,
uuid,
persistentJobOutputFactory.create(tmpTable, uuid),
null,
JobState.QUEUED,
Collections.<Column>emptyList(),
null,
null
);
final Execution execution = new Execution(job,
eventBus,
queryRunnerFactory.create(user.getUserName(), schema),
queryInfoClient,
new QueryExecutionAuthorizer(user, "hive", user.getDefaultSchema()),
timeout,
columnCache,
outputBuilderFactory,
persistorFactory);
executionMap.put(uuid, execution);
activeJobsStore.jobStarted(job);
ListenableFuture<Job> result = executor.submit(execution);
Futures.addCallback(result, new FutureCallback<Job>()
{
@Override
public void onSuccess(@Nullable Job result)
{
if (result != null) {
result.setState(JobState.FINISHED);
}
jobFinished(result);
}
@Override
public void onFailure(@NotNull Throwable t)
{
if (t instanceof ExecutionFailureException) {
ExecutionFailureException e = (ExecutionFailureException) t;
Job j = e.getJob();
j.setState(JobState.FAILED);
if (j.getError() == null) {
j.setError(new QueryError(e.getMessage(), null, -1, null, null, null, null));
}
jobFinished(j);
}
}
});
return uuid;
}
protected void jobFinished(Job job)
{
job.setQueryFinished(new DateTime());
activeJobsStore.jobFinished(job);
historyStore.addRun(job);
for (Table t : job.getTablesUsed()) {
usageStore.markUsage(t);
}
if (job.getOutput() instanceof HiveTablePersistentOutput && job.getOutput().getLocation() != null) {
String[] parts = job.getOutput().getLocation().toString().split("\\.");
if (parts.length == 2) {
Map<String, List<String>> cache = schemaCache.getSchemaMap("hive");
List<String> tables = cache.get(parts[0]);
tables.add(parts[1]);
}
}
eventBus.post(new JobFinishedEvent(job));
executionMap.remove(job.getUuid());
}
public boolean cancelQuery(
AirpalUser user,
UUID uuid)
{
Execution execution = executionMap.get(uuid);
if ((execution != null) && (execution.getJob().getUser().equals(user.getUserName()))) {
execution.cancel();
return true;
} else {
return false;
}
}
public static class ExecutionFailureException extends RuntimeException
{
@Getter
private final Job job;
public ExecutionFailureException(Job job, String message, Throwable cause)
{
super(message, cause);
this.job = job;
}
}
}