package edu.washington.escience.myria.parallel; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import org.joda.time.DateTime; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.collect.ImmutableSet; import edu.washington.escience.myria.DbException; import edu.washington.escience.myria.MyriaConstants; import edu.washington.escience.myria.MyriaConstants.FTMode; import edu.washington.escience.myria.MyriaConstants.ProfilingMode; import edu.washington.escience.myria.RelationKey; import edu.washington.escience.myria.Schema; import edu.washington.escience.myria.Type; import edu.washington.escience.myria.api.encoding.QueryConstruct; import edu.washington.escience.myria.api.encoding.QueryConstruct.ConstructArgs; import edu.washington.escience.myria.api.encoding.QueryEncoding; import edu.washington.escience.myria.api.encoding.QueryStatusEncoding; import edu.washington.escience.myria.api.encoding.QueryStatusEncoding.Status; import edu.washington.escience.myria.coordinator.CatalogException; import edu.washington.escience.myria.coordinator.MasterCatalog; import edu.washington.escience.myria.storage.TupleBuffer; import edu.washington.escience.myria.util.ErrorUtils; /** * Keeps track of all the state (statistics, subqueries, etc.) for a single Myria query. */ public final class Query { /** The logger for this class. */ private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(Query.class); /** The id of this query. */ private final long queryId; /** The id of the next subquery to be issued. */ @GuardedBy("this") private long subqueryId; /** The status of the query. Must be kept synchronized. */ @GuardedBy("this") private Status status; /** The subqueries to be executed. */ @GuardedBy("this") private final LinkedList<SubQuery> subQueryQ; /** The query plan tasks to be executed. */ @GuardedBy("this") private final LinkedList<QueryPlan> planQ; /** The execution statistics about this query. */ @GuardedBy("this") private final ExecutionStatistics executionStats; /** The currently-executing subquery. */ @GuardedBy("this") private SubQuery currentSubQuery; /** The server. */ private final Server server; /** The message explaining why a query failed. */ @GuardedBy("this") private String message; /** The future for this query. */ private final QueryFuture future; /** True if the query should be run with profiling enabled. */ private final Set<ProfilingMode> profiling; /** Indicates whether the query should be run with a particular fault tolerance mode. */ private final FTMode ftMode; /** Global variables that are part of this query. */ private final ConcurrentHashMap<String, Object> globals; /** Temporary relations created during the execution of this query. */ private final ConcurrentHashMap<RelationKey, RelationWriteMetadata> tempRelations; /** resource usage stats of workers. */ private final ConcurrentHashMap<Integer, ConcurrentLinkedDeque<ResourceStats>> resourceUsage; /** * Construct a new {@link Query} object for this query. * * @param queryId the id of this query * @param query contains the query options (profiling, fault tolerance) * @param plan the execution plan * @param server the server on which this query will be executed */ public Query( final long queryId, final QueryEncoding query, final QueryPlan plan, final Server server) { Preconditions.checkNotNull(query, "query"); this.server = Preconditions.checkNotNull(server, "server"); profiling = ImmutableSet.copyOf(query.profilingMode); ftMode = query.ftMode; this.queryId = queryId; subqueryId = 0; synchronized (this) { status = Status.ACCEPTED; } executionStats = new ExecutionStatistics(); subQueryQ = new LinkedList<>(); planQ = new LinkedList<>(); planQ.add(plan); message = null; future = QueryFuture.create(queryId); globals = new ConcurrentHashMap<>(); tempRelations = new ConcurrentHashMap<>(); resourceUsage = new ConcurrentHashMap<Integer, ConcurrentLinkedDeque<ResourceStats>>(); } /** * Return the id of this query. * * @return the id of this query */ public long getQueryId() { return queryId; } /** * Return the status of this query. * * @return the status of this query. */ public synchronized QueryStatusEncoding.Status getStatus() { return status; } /** * Returns <code>true</code> if this query has finished, and <code>false</code> otherwise. * * @return <code>true</code> if this query has finished, and <code>false</code> otherwise */ public synchronized boolean isDone() { return subQueryQ.isEmpty() && planQ.isEmpty(); } /** * Returns the {@link SubQuery} that is currently executing, or <code>null</code> if nothing is running. * * @return the {@link SubQuery} that is currently executing, or <code>null</code> if nothing is running */ public synchronized SubQuery getCurrentSubQuery() { return currentSubQuery; } /** * If the sub-query we're about to execute writes to any persistent relations, generate and enqueue the * "update tuple relation count" sub-query to be run next. * * @param subQuery the subquery about to be executed. This subquery must have already been removed from the queue. * @throws DbException if there is an error getting metadata about existing relations from the Server. */ private synchronized void addDerivedSubQueries(final SubQuery subQuery) throws DbException { Map<RelationKey, RelationWriteMetadata> relationsWritten = currentSubQuery.getPersistentRelationWriteMetadata(server); if (!relationsWritten.isEmpty()) { SubQuery updateCatalog = QueryConstruct.getRelationTupleUpdateSubQuery(relationsWritten, server); subQueryQ.addFirst(updateCatalog); } } /** * Generates and returns the next {@link SubQuery} to run. * * @return the next {@link SubQuery} to run * @throws DbException if there is an error * @throws QueryKilledException if the query has been killed. */ public synchronized SubQuery nextSubQuery() throws DbException, QueryKilledException { Preconditions.checkState( currentSubQuery == null, "must call finishSubQuery before calling nextSubQuery"); if (isDone()) { return null; } if (status == Status.KILLING) { throw new QueryKilledException(); } if (!subQueryQ.isEmpty()) { currentSubQuery = subQueryQ.removeFirst(); SubQueryId sqId = new SubQueryId(queryId, subqueryId); currentSubQuery.setSubQueryId(sqId); addDerivedSubQueries(currentSubQuery); Set<ProfilingMode> profilingMode = getProfilingMode(); if (!profilingMode.isEmpty()) { if (!currentSubQuery.isProfileable()) { profilingMode = ImmutableSet.of(); } else { server.setQueryPlan(sqId, currentSubQuery.getEncodedPlan()); } } QueryConstruct.setQueryExecutionOptions( currentSubQuery.getWorkerPlans(), ftMode, profilingMode); currentSubQuery.getMasterPlan().setFTMode(ftMode); currentSubQuery.getMasterPlan().setProfilingMode(ImmutableSet.<ProfilingMode>of()); ++subqueryId; if (subqueryId >= MyriaConstants.MAXIMUM_NUM_SUBQUERIES) { throw new DbException( "Infinite-loop safeguard: quitting after " + MyriaConstants.MAXIMUM_NUM_SUBQUERIES + " subqueries."); } return currentSubQuery; } planQ.getFirst().instantiate(planQ, subQueryQ, new ConstructArgs(server, queryId)); /* The above line may have emptied planQ, mucked with subQueryQ, not sure. So just recurse to make sure we do the * right thing. */ return nextSubQuery(); } /** * Mark the current {@link SubQuery} as finished. */ public synchronized void finishSubQuery() { currentSubQuery = null; } /** * Returns the time this query started, in ISO8601 format, or <code>null</code> if the query has not yet been started. * * @return the time this query started, in ISO8601 format, or <code>null</code> if the query has not yet been started */ public synchronized DateTime getStartTime() { return executionStats.getStartTime(); } /** * Set the time this query started to now in ISO8601 format. */ public synchronized void markStart() { status = Status.RUNNING; executionStats.markStart(); } /** * Set this query as having failed due to the specified cause. * * @param cause the reason the query failed. */ public synchronized void markFailed(final Throwable cause) { Preconditions.checkNotNull(cause, "cause"); if (Status.finished(status)) { LOGGER.warn( "Ignoring markFailed({}) because already finished: status {}, message {}", cause, status, message); return; } status = Status.ERROR; markEnd(); message = ErrorUtils.getStackTrace(cause); future.setException(cause); } /** * Set the time this query ended to now in ISO8601 format. */ public synchronized void markSuccess() { Verify.verify(currentSubQuery == null, "expect current subquery to be null when query ends"); if (Status.finished(status)) { LOGGER.warn( "Ignoring markSuccess() because already finished: status {}, message {}", status, message); return; } status = Status.SUCCESS; markEnd(); future.set(this); } /** * Set the time this query ended to now in ISO8601 format. */ private synchronized void markEnd() { executionStats.markEnd(); } /** * Returns the time this query ended, in ISO8601 format, or <code>null</code> if the query has not yet ended. * * @return the time this query ended, in ISO8601 format, or <code>null</code> if the query has not yet ended */ public synchronized DateTime getEndTime() { return executionStats.getEndTime(); } /** * Returns the time elapsed (in nanoseconds) since the query started. * * @return the time elapsed (in nanoseconds) since the query started */ public synchronized Long getElapsedTime() { return executionStats.getQueryExecutionElapse(); } /** * Return a message explaining why a query failed, or <code>null</code> if the query did not fail. * * @return a message explaining why a query failed, or <code>null</code> if the query did not fail */ public synchronized String getMessage() { return message; } /** * Returns the future on the execution of this query. * * @return the future on the execution of this query */ public QueryFuture getFuture() { return future; } /** * Call when the query has been killed. */ public synchronized void markKilled() { markEnd(); if (Status.finished(status)) { LOGGER.warn( "Ignoring markKilled() because already finished: status {}, message {}", status, message); return; } Preconditions.checkState( status == Status.KILLING, "cannot mark a query killed unless its status is KILLING, not %s", status); status = Status.KILLED; future.cancel(true); } /** * @return the fault tolerance mode for this query. */ protected FTMode getFTMode() { return ftMode; } /** * @return true if this query should be profiled. */ @Nonnull protected Set<ProfilingMode> getProfilingMode() { return profiling; } /** * Return the value of the global variable named by the specified key. * * @param key the name of the variable * @return the value of the variable, nor {@code null} if the variable does not exist. */ public Object getGlobal(final String key) { return globals.get(key); } /** * Set the global variable named by the specified key to the specified value. * * @param key the name of the variable * @param value the new value for the variable */ public void setGlobal(final String key, final Object value) { globals.put(key, value); } /** * Initiate the process of killing this query, if it's not done already. */ public synchronized void kill() { if (!Status.ongoing(status)) { LOGGER.warn( "Ignoring kill() because query is not ongoing; status {}, message {}", status, message); return; } status = Status.KILLING; if (currentSubQuery != null) { server.getQueryManager().killSubQuery(currentSubQuery.getSubQueryId()); currentSubQuery = null; } } /** * Determines and sanity-checks the set of relations created by the currently running subquery. Assuming the checks * pass, creates a {@link DatasetMetadataUpdater} future for this subquery and adds it as a pre-listener to the * specified {@code future}. * * @param catalog the Catalog in which the relation metadata will be updated * @param future the future on the subquery * @throws DbException if there is an error */ public synchronized void addDatasetMetadataUpdater( final MasterCatalog catalog, final LocalSubQueryFuture future) throws DbException { SubQuery subQuery = currentSubQuery; final Map<RelationKey, RelationWriteMetadata> relationsCreated = subQuery.getRelationWriteMetadata(server); if (relationsCreated.size() == 0) { return; } /* Verify that the schemas for any temp relation we're not overwriting match the existing schema. */ for (RelationWriteMetadata meta : relationsCreated.values()) { if (meta.isOverwrite()) { if (meta.isTemporary()) { tempRelations.put(meta.getRelationKey(), meta); } continue; } RelationKey relation = meta.getRelationKey(); Schema oldSchema; String relationType; if (meta.isTemporary()) { relationType = "temporary"; oldSchema = tempRelations.get(relation).getSchema(); } else { relationType = "persistent"; try { oldSchema = server.getSchema(relation); } catch (CatalogException e) { throw new DbException( Joiner.on(' ') .join( "Error checking catalog for schema of", relation, "during subquery", subQuery.getSubQueryId()), e); } } if (oldSchema != null) { Preconditions.checkArgument( oldSchema.equals(meta.getSchema()), "Cannot append to existing %s relation %s (schema: %s) with new schema (%s)", relationType, relation, oldSchema, meta.getSchema()); } } Map<RelationKey, RelationWriteMetadata> persistentRelations = subQuery.getPersistentRelationWriteMetadata(server); if (persistentRelations.size() == 0) { return; } /* Add the DatasetMetadataUpdater, which will update the catalog with the set of workers created when the query * succeeds. Note that we only use persistent relations here. */ DatasetMetadataUpdater dsmd = new DatasetMetadataUpdater(catalog, persistentRelations, subQuery.getSubQueryId()); future.addPreListener(dsmd); } /** * Returns the schema for the specified temp relation. * * @param relationKey the desired temp relation * @return the schema for the specified temp relation */ public Schema getTempSchema(@Nonnull final RelationKey relationKey) { return getMetadata(relationKey).getSchema(); } /** * Returns the workers storing the specified temp relation. * * @param relationKey the desired temp relation * @return the set of workers storing the specified temp relation */ public @Nonnull Set<Integer> getWorkersForTempRelation(@Nonnull final RelationKey relationKey) { return getMetadata(relationKey).getWorkers(); } /** * Get the {@link RelationWriteMetadata} for the specified temp relation. * * @param relationKey the key of the temp relation * @return the {@link RelationWriteMetadata} for the specified temp relation * @throws IllegalArgumentException if there is no such temp relation */ private RelationWriteMetadata getMetadata(@Nonnull final RelationKey relationKey) { Preconditions.checkNotNull(relationKey, "relationKey"); RelationWriteMetadata meta = tempRelations.get(relationKey); Preconditions.checkArgument( meta != null, "Query #%s, no temp relation with key %s found", queryId, relationKey); return meta; } /** * @param senderId from whicht worker the stats were sent from * @param stats the stats */ public void addResourceStats(final int senderId, final ResourceStats stats) { resourceUsage.putIfAbsent(senderId, new ConcurrentLinkedDeque<ResourceStats>()); resourceUsage.get(senderId).add(stats); } /** * @return resource usage stats in a tuple buffer. */ public TupleBuffer getResourceUsage() { Schema schema = Schema.appendColumn(MyriaConstants.RESOURCE_PROFILING_SCHEMA, Type.INT_TYPE, "workerId"); TupleBuffer tb = new TupleBuffer(schema); for (int workerId : resourceUsage.keySet()) { ConcurrentLinkedDeque<ResourceStats> statsList = resourceUsage.get(workerId); for (ResourceStats stats : statsList) { tb.putLong(0, stats.getTimestamp()); tb.putInt(1, stats.getOpId()); tb.putString(2, stats.getMeasurement()); tb.putLong(3, stats.getValue()); tb.putLong(4, stats.getQueryId()); tb.putLong(5, stats.getSubqueryId()); tb.putInt(6, workerId); } } return tb; } }