com.indeed.imhotep.web.ExecutionManager.java Source code

Java tutorial

Introduction

Here is the source code for com.indeed.imhotep.web.ExecutionManager.java

Source

/*
 * Copyright (C) 2014 Indeed Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.indeed.imhotep.web;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Keeps track of the currently running queries.
 * All public methods on this class operating on internal data structures should be marked synchronized.
 * @author vladimir
 */
@Component
public class ExecutionManager {
    private static final Logger log = Logger.getLogger(ExecutionManager.class);
    // values are used for locking to avoid concurrent processing of identical requests
    private final Map<String, CountDownLatch> queryToLock = Maps.newHashMap();
    // used to limit number of concurrent queries per user
    private final Map<String, Semaphore> userToLock = Maps.newHashMap();
    private final Set<QueryTracker> runningQueries = Sets.newHashSet();

    @Value("${user.concurrent.query.limit}")
    private int maxQueriesPerUser;

    public ExecutionManager() {
    }

    @Nonnull
    public synchronized List<QueryTracker> getRunningQueries() {
        return Lists.newArrayList(runningQueries);
    }

    /**
     * Keeps track of the query that is going to be executed and makes sure that user allocated limit is not exceeded.
     * When the query execution is completed (data is in HDFS cache) or fails, the returned Query object must be closed.
     */
    public synchronized QueryTracker queryStarted(String query, String username) throws TimeoutException {
        final CountDownLatch releaseLock;
        final CountDownLatch waitLockForQuery;
        final Semaphore waitLockForUser;
        if (queryToLock.containsKey(query)) { // this is a duplicate query and execution will have to wait
            waitLockForQuery = queryToLock.get(query);
            waitLockForUser = null;
            releaseLock = null;
        } else { // this is a non-duplicate query and the lock has to be released after execution is finished
            waitLockForQuery = null;
            waitLockForUser = getUserSemaphore(username);
            releaseLock = new CountDownLatch(1);
            queryToLock.put(query, releaseLock);
        }
        final QueryTracker newQueryTracker = new QueryTracker(username, query, waitLockForQuery, waitLockForUser,
                releaseLock, this);
        runningQueries.add(newQueryTracker);
        return newQueryTracker;
    }

    private synchronized Semaphore getUserSemaphore(String username) {
        Semaphore semaphore = userToLock.get(username);
        if (semaphore == null) {
            semaphore = new Semaphore(maxQueriesPerUser, true);
            userToLock.put(username, semaphore);
        }
        return semaphore;
    }

    private synchronized void release(QueryTracker q) {
        if (q.released) {
            return; // release called twice
        }
        q.released = true;

        if (q.releaseLock != null) { // this was the original query of this type and it's done now
            q.releaseLock.countDown();
            queryToLock.remove(q.query);
        }
        if (q.userSlotUsed && q.waitLockForUser != null) {
            q.waitLockForUser.release();
        }

        runningQueries.remove(q);
    }

    /**
     * Keeps track of the query execution.
     * Must be closed when all operations relating to the query processing are complete (including HDFS cache upload).
     */
    public class QueryTracker implements Closeable {
        private final String username; // user running the query
        private final String query; // query text
        private final CountDownLatch waitLockForQuery; // lock to wait on before the query starts
        private final Semaphore waitLockForUser; // lock to wait on before the query starts
        private final CountDownLatch releaseLock; // lock to be released when the query is done
        private final ExecutionManager owner;
        private final DateTime startedTime = DateTime.now();
        private boolean asynchronousRelease = false;
        private boolean released = false;
        private boolean userSlotUsed = false;

        private QueryTracker(String username, String query, CountDownLatch waitLockForQuery,
                Semaphore waitLockForUser, CountDownLatch releaseLock, ExecutionManager owner) {
            this.username = username;
            this.query = query;
            this.waitLockForQuery = waitLockForQuery;
            this.waitLockForUser = waitLockForUser;
            this.releaseLock = releaseLock;
            this.owner = owner;
        }

        public String getUsername() {
            return username;
        }

        public String getQuery() {
            return query;
        }

        public String getStartedTime() {
            return startedTime.toString();
        }

        public void acquireLocks() throws TimeoutException {
            waitForQueryLock();
            waitForUserLock();
        }

        private void waitForQueryLock() throws TimeoutException {
            if (waitLockForQuery == null) {
                return;
            }
            // same query is already being handled, waiting
            try {
                if (!waitLockForQuery.await(5, TimeUnit.MINUTES)) {
                    log.error("Reached timeout waiting for completion of: " + query);
                    throw new TimeoutException(
                            "Reached timeout (5 min) waiting for completion of original execution of the query");
                }
            } catch (InterruptedException ignored) {
                throw new RuntimeException(
                        "Interrupted while waiting for completion of original execution of the query. You can retry.");
            }
        }

        private void waitForUserLock() throws TimeoutException {
            if (waitLockForUser == null) {
                return;
            }

            try {
                if (!waitLockForUser.tryAcquire(5, TimeUnit.MINUTES)) {
                    throw new TimeoutException("Reached timeout (5 min) waiting in queue for query execution");
                }
                userSlotUsed = true;
            } catch (InterruptedException ignored) {
                throw new RuntimeException("Wait in queue for query execution was interrupted. You can retry.");
            }
        }

        @Override
        public void close() throws IOException {
            owner.release(this);
        }

        public void markAsynchronousRelease() {
            this.asynchronousRelease = true;
        }

        @JsonIgnore
        public boolean isAsynchronousRelease() {
            return asynchronousRelease;
        }
    }
}