/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.sa.engine.scheduler;
import com.emc.sa.model.util.ScheduledTimeComparator;
import com.emc.storageos.db.client.model.uimodels.*;
import com.emc.sa.model.dao.ModelClient;
import com.emc.storageos.db.client.util.ExecutionWindowHelper;
import com.emc.storageos.db.client.model.NamedURI;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class SchedulerDataManager {
private static final Logger LOG = LoggerFactory.getLogger(SchedulerDataManager.class);
/** The lock for accessing shared data. */
private final Lock lock = new ReentrantLock();
/** The condition for waiting on active windows. */
private final Condition hasActiveWindows = lock.newCondition();
/** Model access. */
@Autowired
private ModelClient models;
/** The map of currently active windows. */
private Map<URI, ExecutionWindow> activeWindows = Maps.newHashMap();
/** The set of currently processing orders. */
private Set<URI> activeOrders = Sets.newHashSet();
private boolean enableInfiniteExecutionWindow = true;
/**
* Gets all execution windows from the database.
*
* @return the list of all execution windows.
*/
protected List<ExecutionWindow> getAllExecutionWindows() {
List<URI> ids = models.findByType(ExecutionWindow.class);
return models.findByIds(ExecutionWindow.class, ids);
}
/**
* Updates the active windows.
*
* @see #getAllExecutionWindows()
*/
public void updateActiveWindows() {
LOG.debug("Updating active windows");
List<ExecutionWindow> allWindows = getAllExecutionWindows();
Map<URI, ExecutionWindow> currentWindows = getCurrentActiveWindows();
Calendar currentTime = Calendar.getInstance();
for (ExecutionWindow window : allWindows) {
boolean active = isActive(window, currentTime);
if (active) {
activateWindow(window);
}
else {
deactivateWindow(window);
}
currentWindows.remove(window.getId());
}
// Deactivate any execution windows which aren't in the list of all windows
for (ExecutionWindow window : currentWindows.values()) {
deactivateWindow(window);
}
}
/**
* Waits for active windows to become available. This will not return until at least one window is active.
*
* @return the mapping of active windows.
*
* @throws InterruptedException
*/
protected Map<URI, ExecutionWindow> waitForActiveWindows() throws InterruptedException {
Map<URI, ExecutionWindow> windows = Maps.newHashMap();
while (windows.isEmpty()) {
lock.lock();
try {
if (activeWindows.isEmpty() && enableInfiniteExecutionWindow == false ) {
hasActiveWindows.await();
}
windows.putAll(activeWindows);
if (enableInfiniteExecutionWindow == true) {
return windows;
}
} finally {
lock.unlock();
}
}
return windows;
}
/**
* Determines if the window is active at the given time.
*
* @param window
* the execution window.
* @param time
* the time.
* @return true if the window is active.
*/
protected boolean isActive(ExecutionWindow window, Calendar time) {
ExecutionWindowHelper helper = new ExecutionWindowHelper(window);
return helper.isActive(time);
}
/**
* Gets the execution windows that are currently active.
*
* @return the currently active execution windows.
*/
protected Map<URI, ExecutionWindow> getCurrentActiveWindows() {
Map<URI, ExecutionWindow> currentWindows = Maps.newHashMap();
lock.lock();
try {
currentWindows.putAll(activeWindows);
} finally {
lock.unlock();
}
return currentWindows;
}
/**
* Activates an execution window, if required. If the window is not already active, it is added to the active
* windows and all waiting objects are notified.
*
* @param window
* the window to activate.
*/
protected void activateWindow(ExecutionWindow window) {
lock.lock();
try {
if (!activeWindows.containsKey(window.getId())) {
LOG.info("Activate window: " + window.getLabel());
activeWindows.put(window.getId(), window);
hasActiveWindows.signalAll();
}
} finally {
lock.unlock();
}
}
/**
* Deactivates an execution window, if required. If the window is currently active, it is removed from the active
* windows and all waiting objects are notified.
*
* @param window
*/
protected void deactivateWindow(ExecutionWindow window) {
lock.lock();
try {
if (activeWindows.containsKey(window.getId())) {
LOG.info("Deactivate window: " + window.getLabel());
activeWindows.remove(window.getId());
}
} finally {
lock.unlock();
}
}
/**
* Gets and locks the next scheduled order that can be executed. This will wait until an order becomes available.
* Once an order is returned from this method, the tenant will be locked until {@link #unlockOrder(Order)} is
* called.
*
* @return the next scheduled order.
*
* @throws InterruptedException
* if the thread is interrupted.
*/
public Order lockNextScheduledOrder() throws InterruptedException {
Order order = null;
while (order == null) {
order = tryGetNextScheduledOrder();
if (order == null) {
Thread.sleep(15000);
}
}
LOG.debug("Locked order " + order.getId());
return order;
}
/**
* Signals that the order is finished, releasing the tenant lock.
*
* @param order
* the order.
*/
public void unlockOrder(Order order) {
LOG.debug("Unlocking order " + order.getId());
doUnlockOrder(order);
}
/**
* Checks for a scheduled order that can be run in any of the active windows. Any order returned from this method
* will have its tenant locked until {@link #unlockOrder(Order)} is called.
*
* @return the next order to be run, or null if none are available.
*
* @throws InterruptedException
* if the thread is interrupted.
*/
protected Order tryGetNextScheduledOrder() throws InterruptedException {
// It would include normal active window and special INFINITE execution window
Map<URI, ExecutionWindow> windows = waitForActiveWindows();
// Pull all orders from the database and match orders to tenant and execution windows
List<Order> orders = getAllScheduledOrders();
lock.lock();
try {
Set<String> activeTenants = new HashSet<String>();
if (windows.size() != 0) {
// Get the tenants that are currently active within execution windows
activeTenants = getTenants(windows.values());
}
Calendar currTime = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
for (Order order : orders) {
LOG.debug("The order should be scheduled at {}", order.getScheduledTime());
if (currTime.before(order.getScheduledTime())) {
LOG.debug("It is not time to invoke the earliest order yet.");
break;
}
if (order.getExecutionWindowId() == null) {
// order is not subjected to normal execution window but the special INFINITE window.
// check if the order is expired
if (isExpiredOrder(order, null)) {
order.setOrderStatus(OrderStatus.ERROR.name());
models.save(order);
continue;
}
// lock a order
if (!isLocked(order)) {
if (lockOrder(order)) {
return order;
}
}
} else {
// order is subjected to normal execution window
if (windows.size() == 0) {
continue;
}
// lock a order if tenant is active and window is matched.
boolean matchesTenant = activeTenants.contains(order.getTenant());
if (matchesTenant && !isLocked(order) && canRunInWindow(order, windows)) {
if (lockOrder(order)) {
return order;
}
}
}
}
} finally {
lock.unlock();
}
// No matching orders
return null;
}
/**
* Determines if the order is locked.
*
* @param order
* the order.
* @return true if the order is locked.
*/
private boolean isLocked(Order order) {
lock.lock();
try {
return activeOrders.contains(order.getId());
} finally {
lock.unlock();
}
}
/**
* Attempts the lock the order. If the order is already active this returns false.
*
* @param order
* the order.
* @return true if the order is successfully locked.
*/
private boolean lockOrder(Order order) {
lock.lock();
try {
return activeOrders.add(order.getId());
} finally {
lock.unlock();
}
}
/**
* Unlocks the order.
*
* @param order
* the order.
*/
private void doUnlockOrder(Order order) {
lock.lock();
try {
activeOrders.remove(order.getId());
} finally {
lock.unlock();
}
}
/**
* Gets the set of tenants from the execution windows.
*
* @param windows
* the execution windows.
* @return the set of tenants.
*/
protected Set<String> getTenants(Collection<ExecutionWindow> windows) {
Set<String> tenants = Sets.newHashSet();
for (ExecutionWindow window : windows) {
tenants.add(window.getTenant());
}
return tenants;
}
/**
* Gets all scheduled orders from the database, ordered.
*
* @return the list of all scheduled orders.
*/
protected List<Order> getAllScheduledOrders() {
List<Order> orders = models.orders().findByOrderStatus(OrderStatus.SCHEDULED);
Collections.sort(orders, ScheduledTimeComparator.OLDEST);
return orders;
}
/**
* Determines if the order can run in any of the given execution windows.
*
* @param order
* the order.
* @param windows
* the execution windows.
* @return true if the order can run in the windows.
*/
protected boolean canRunInWindow(Order order, Map<URI, ExecutionWindow> windows) {
NamedURI windowId = order.getExecutionWindowId();
if (ExecutionWindow.isNextWindow(windowId)) {
LOG.debug("Matches NEXT window");
return true;
}
else if ((windowId != null) && windows.containsKey(windowId.getURI())) {
LOG.debug("Matches '" + windowId.getName() + "' window");
return true;
}
else {
return false;
}
}
private boolean isExpiredOrder(Order order, ExecutionWindow window) {
ExecutionWindowHelper windowHelper = new ExecutionWindowHelper(window);
if (windowHelper.isExpired(order.getScheduledTime())) {
order.setOrderStatus(OrderStatus.ERROR.name());
LOG.info("order {} has expired.", order.getId());
models.save(order);
return true;
}
return false;
}
/**
* Gets all REOCCURRENCE scheduled events from the database, ordered.
*
* @return the list of all scheduled events
*/
public List<ScheduledEvent> getAllReoccurrenceEvents() {
List<ScheduledEvent> scheduledEvents = models.scheduledEvents().findByScheduledEventType(ScheduledEventType.REOCCURRENCE);
return scheduledEvents;
}
}