/*
* Copyright 2008-2010 LinkedIn, 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 voldemort.server.protocol.admin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.log4j.Logger;
import voldemort.VoldemortException;
import voldemort.annotations.jmx.JmxGetter;
import voldemort.annotations.jmx.JmxManaged;
import voldemort.annotations.jmx.JmxOperation;
import voldemort.common.service.AbstractService;
import voldemort.common.service.SchedulerService;
import voldemort.common.service.ServiceType;
import com.google.common.collect.ImmutableSet;
/**
* Asynchronous job scheduler for admin service operations.
*
* TODO: requesting a unique id, then creating an operation with that id seems
* like a bad API design.
*
*/
@JmxManaged(description = "Asynchronous operation execution")
public class AsyncOperationService extends AbstractService {
private final Map<Integer, AsyncOperation> operations;
private final AtomicInteger lastOperationId = new AtomicInteger(0);
private final SchedulerService scheduler;
private final static Logger logger = Logger.getLogger(AsyncOperationService.class);
public AsyncOperationService(SchedulerService scheduler, int cacheSize) {
super(ServiceType.ASYNC_SCHEDULER);
operations = Collections.synchronizedMap(new AsyncOperationCache(cacheSize));
this.scheduler = scheduler;
}
/**
* Submit a operations. Throw a run time exception if the operations is
* already submitted
*
* @param operation The asynchronous operations to submit
* @param requestId Id of the request
*/
public synchronized void submitOperation(int requestId, AsyncOperation operation) {
if(this.operations.containsKey(requestId))
throw new VoldemortException("Request " + requestId
+ " already submitted to the system");
this.operations.put(requestId, operation);
scheduler.scheduleNow(operation);
logger.debug("Handling async operation " + requestId);
}
/**
* Check if the an operation is done or not.
*
* @param requestId Id of the request
* @param remove Whether remove the request out of the list if it is done.
* @return True if request is complete, false otherwise
*/
public synchronized boolean isComplete(int requestId, boolean remove) {
if (!operations.containsKey(requestId))
throw new VoldemortException("No operation with id " + requestId + " found");
if (operations.get(requestId).getStatus().isComplete()) {
if (logger.isDebugEnabled())
logger.debug("Operation complete " + requestId);
if (remove)
operations.remove(requestId);
return true;
}
return false;
}
/**
* A default caller. remove operations ID out of the list when it is done.
* @param requestId Id of the request
*/
public synchronized boolean isComplete(int requestId) {
return isComplete(requestId, true);
}
// Wrap getOperationStatus to avoid throwing exception over JMX
@JmxOperation(description = "Retrieve operation status")
public String getStatus(int id) {
try {
return getOperationStatus(id).toString();
} catch(VoldemortException e) {
return "No operation with id " + id + " found";
}
}
public List<Integer> getMatchingAsyncOperationList(String jobDescPattern, boolean showCompleted) {
List<Integer> operationIds = getAsyncOperationList(showCompleted);
List<Integer> matchingOperationIds = new ArrayList<Integer>(operationIds.size());
for(Integer operationId: operationIds) {
AsyncOperation operation = operations.get(operationId);
String operationDescription = operation.getStatus().getDescription();
if(operationDescription != null && operationDescription.indexOf(jobDescPattern) != -1) {
matchingOperationIds.add(operationId);
}
}
return matchingOperationIds;
}
@JmxOperation(description = "Retrieve all operations")
public String getAllAsyncOperations() {
String result;
try {
result = operations.toString();
} catch(Exception e) {
result = e.getMessage();
}
return result;
}
/**
* Get list of asynchronous operations on this node. By default, only the
* pending operations are returned.
*
* @param showCompleted Show completed operations
* @return A list of operation ids.
*/
public List<Integer> getAsyncOperationList(boolean showCompleted) {
/**
* Create a copy using an immutable set to avoid a
* {@link java.util.ConcurrentModificationException}
*/
Set<Integer> keySet = ImmutableSet.copyOf(operations.keySet());
if(showCompleted)
return new ArrayList<Integer>(keySet);
List<Integer> keyList = new ArrayList<Integer>();
for(int key: keySet) {
AsyncOperation operation = operations.get(key);
if(operation != null && !operation.getStatus().isComplete())
keyList.add(key);
}
return keyList;
}
public AsyncOperationStatus getOperationStatus(int requestId) {
if(!operations.containsKey(requestId))
throw new VoldemortException("No operation with id " + requestId + " found");
return operations.get(requestId).getStatus();
}
// Wrapper to avoid throwing an exception over JMX
@JmxOperation
public String stopAsyncOperation(int requestId) {
try {
stopOperation(requestId);
} catch(VoldemortException e) {
return e.getMessage();
}
return "Stopping operation " + requestId;
}
public void stopOperation(int requestId) {
if(!operations.containsKey(requestId))
throw new VoldemortException("No operation with id " + requestId + " found");
operations.get(requestId).stop();
}
/**
* Generate a unique request id
*
* @return A new, guaranteed unique, request id
*/
public int getUniqueRequestId() {
return lastOperationId.getAndIncrement();
}
@Override
protected void startInner() {
logger.info("Starting asyncOperationRunner");
}
@Override
protected void stopInner() {
logger.info("Stopping asyncOperationRunner");
}
@JmxGetter(name = "totalWaitTime",
description = "Cumulative number of seconds spent by all tasks in Queue Waiting")
public long totalWaitTime() {
long totalWaitTimeMs = 0;
Set<Integer> keySet = ImmutableSet.copyOf(operations.keySet());
for(int key: keySet) {
AsyncOperation operation = operations.get(key);
if(operation != null && !operation.getStatus().isComplete()) {
totalWaitTimeMs += operation.getWaitTimeMs();
}
}
return TimeUnit.SECONDS.convert(totalWaitTimeMs, TimeUnit.MILLISECONDS);
}
@JmxGetter(name = "numWaitingTasks", description = "Total number of tasks waiting in the Queue")
public long waitingTasks() {
long waitingTasks = 0;
Set<Integer> keySet = ImmutableSet.copyOf(operations.keySet());
for(int key: keySet) {
AsyncOperation operation = operations.get(key);
if(operation != null && !operation.getStatus().isComplete()) {
if(operation.isWaiting())
waitingTasks++;
}
}
return waitingTasks;
}
}