package org.ovirt.engine.ui.frontend.server.dashboard;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Random;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.enterprise.concurrent.ManagedScheduledExecutorService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.map.ObjectMapper;
import org.infinispan.Cache;
import org.infinispan.manager.CacheContainer;
import org.ovirt.engine.core.utils.EngineLocalConfig;
import org.ovirt.engine.ui.frontend.server.dashboard.fake.FakeDataGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DashboardDataServlet extends HttpServlet {
/**
* SerialVersionUID
*/
private static final long serialVersionUID = 6678850917843141114L;
private static final Logger log = LoggerFactory.getLogger(DashboardDataServlet.class);
private static final String CONTENT_TYPE = "application/json"; //$NON-NLS-1$
private static final String ENCODING = "UTF-8"; //$NON-NLS-1$
private static final String DASHBOARD = "dashboard"; //$NON-NLS-1$
private static final String INVENTORY = "inventory"; //$NON-NLS-1$
private static final String UTILIZATION_KEY = "utilization_key"; //$NON-NLS-1$
private static final String INVENTORY_KEY = "inventory_key"; //$NON-NLS-1$
private static final Object UTILIZATION_LOCK = new Object();
private static final String ENABLE_CACHE_UPDATE_KEY = "DASHBOARD_CACHE_UPDATE"; //$NON-NLS-1$
private static final String UTILIZATION_CACHE_UPDATE_INTERVAL_KEY = "DASHBOARD_UTILIZATION_CACHE_UPDATE_INTERVAL"; //$NON-NLS-1$
private static final String INVENTORY_CACHE_UPDATE_INTERVAL_KEY = "DASHBOARD_INVENTORY_CACHE_UPDATE_INTERVAL"; //$NON-NLS-1$
private static long UTILIZATION_CACHE_UPDATE_INTERVAL;
private static long INVENTORY_CACHE_UPDATE_INTERVAL;
private static final String PREFER_HEADER = "Prefer"; //$NON-NLS-1$
private static final String PREFER_FAKE_DATA = "fake_data"; //$NON-NLS-1$
private static final String PREFER_ERROR = "error"; //$NON-NLS-1$
private static final String PREFER_NO_CACHE = "nocache"; //$NON-NLS-1$
@Resource(mappedName = "java:/DWHDataSource")
private DataSource dwhDataSource;
@Resource(mappedName = "java:/ENGINEDataSource")
private DataSource engineDataSource;
private boolean dwhAvailable = false;
private boolean enableBackgroundCacheUpdate = false;
@Resource(lookup = "java:jboss/infinispan/ovirt-engine")
private CacheContainer cacheContainer;
private Cache<String, Dashboard> dashboardCache;
private Cache<String, Inventory> inventoryCache;
@Resource
private ManagedScheduledExecutorService scheduledExecutor;
private ScheduledFuture<?> utilizationCacheUpdate = null;
private ScheduledFuture<?> inventoryCacheUpdate = null;
@PostConstruct
private void initCache() {
dashboardCache = cacheContainer.getCache(DASHBOARD);
inventoryCache = cacheContainer.getCache(INVENTORY);
dwhAvailable = checkDwhConfigInEngine() && checkDwhDataSource();
EngineLocalConfig config = EngineLocalConfig.getInstance();
try {
enableBackgroundCacheUpdate = config.getBoolean(ENABLE_CACHE_UPDATE_KEY, Boolean.FALSE);
} catch (IllegalArgumentException e) {
log.error("Missing/Invalid key \"{}\", using default value of 'false'", ENABLE_CACHE_UPDATE_KEY, e); //$NON-NLS-1$
enableBackgroundCacheUpdate = false;
}
if (!enableBackgroundCacheUpdate || !dwhAvailable) {
log.info("Dashboard DB query cache has been disabled."); //$NON-NLS-1$
return;
}
/*
* Update the utilization cache now and every 5 minutes (by default) thereafter, but never run 2 updates simultaneously.
*/
try {
UTILIZATION_CACHE_UPDATE_INTERVAL = config.getLong(UTILIZATION_CACHE_UPDATE_INTERVAL_KEY);
} catch (IllegalArgumentException e) {
log.error("Missing/Invalid key \"{}\", using default value of 300", UTILIZATION_CACHE_UPDATE_INTERVAL_KEY, e); //$NON-NLS-1$
UTILIZATION_CACHE_UPDATE_INTERVAL = 300;
}
utilizationCacheUpdate = scheduledExecutor.scheduleWithFixedDelay(new Runnable() {
Logger log = LoggerFactory.getLogger(DashboardDataServlet.class.getName() + ".CacheUpdate.Utilization"); //$NON-NLS-1$
@Override
public void run() {
log.trace("Attempting to update the Utilization cache"); //$NON-NLS-1$
try {
populateUtilizationCache();
} catch (DashboardDataException e) {
log.error("Could not update the Utilization Cache: {}", e.getMessage(), e); //$NON-NLS-1$
}
}
}, 0, UTILIZATION_CACHE_UPDATE_INTERVAL, TimeUnit.SECONDS);
log.info("Dashboard utilization cache updater initialized (update interval {}s)", UTILIZATION_CACHE_UPDATE_INTERVAL); //$NON-NLS-1$
/*
* Update the inventory cache now and every 60 seconds (by default) thereafter, but never run 2 updates simultaneously.
*/
try {
INVENTORY_CACHE_UPDATE_INTERVAL = config.getLong(INVENTORY_CACHE_UPDATE_INTERVAL_KEY);
} catch (IllegalArgumentException e) {
log.error("Missing/Invalid key \"{}\", using default value of 60", INVENTORY_CACHE_UPDATE_INTERVAL_KEY, e); //$NON-NLS-1$
INVENTORY_CACHE_UPDATE_INTERVAL = 60;
}
inventoryCacheUpdate = scheduledExecutor.scheduleWithFixedDelay(new Runnable() {
Logger log = LoggerFactory.getLogger(DashboardDataServlet.class.getName() + ".CacheUpdate.Inventory"); //$NON-NLS-1$
@Override
public void run() {
log.trace("Attempting to update the Inventory cache"); //$NON-NLS-1$
try {
populateInventoryCache();
} catch (DashboardDataException e) {
log.error("Could not update the Inventory Cache: {}", e.getMessage(), e); //$NON-NLS-1$
}
}
}, 0, INVENTORY_CACHE_UPDATE_INTERVAL, TimeUnit.SECONDS);
log.info("Dashboard inventory cache updater initialized (update interval {}s)", INVENTORY_CACHE_UPDATE_INTERVAL); //$NON-NLS-1$
}
/**
* Check that <b>dwhHostname</b> and <b>dwhUuid</b> have been both defined in the Engine
* table <b>dwh_history_timekeeping</b>. These two records only have values defined when
* the engine has been configured with DWH.
*/
private boolean checkDwhConfigInEngine() {
boolean isOk = true;
String hostname = null;
String uuid = null;
try (Connection c = engineDataSource.getConnection();
Statement s = c.createStatement();
ResultSet rs = s.executeQuery("SELECT * FROM dwh_history_timekeeping WHERE var_name IN ('dwhHostname', 'dwhUuid')")) { //$NON-NLS-1$
while (rs.next()) {
String varName = StringUtils.trimToNull(rs.getString("var_name")); //$NON-NLS-1$
String varValue = StringUtils.trimToNull(rs.getString("var_value")); //$NON-NLS-1$
switch (varName) {
case "dwhHostname": //$NON-NLS-1$
hostname = varValue;
break;
case "dwhUuid": //$NON-NLS-1$
uuid = varValue;
break;
}
}
} catch (SQLException e) {
log.error("Could not access engine's DWH configuration table", e); //$NON-NLS-1$
}
if (StringUtils.isEmpty(hostname) || StringUtils.isEmpty(uuid)) {
log.warn("No valid DWH configurations were found, assuming DWH database isn't setup."); //$NON-NLS-1$
isOk = false;
}
return isOk;
}
/**
* Check that the JNDI managed DWH DataSource has been injected and that it
* can successfully open a connection to the DWH database.
*/
private boolean checkDwhDataSource() {
boolean isOk;
if (dwhDataSource == null) {
log.warn("DWH DataSource has not been configured and injected."); //$NON-NLS-1$
isOk = false;
} else {
try (Connection c = dwhDataSource.getConnection()) {
isOk = true;
}
catch (SQLException e) {
log.warn("Could not establish a connection to the DWH via the DWH DataSource: {}", e.getMessage()); //$NON-NLS-1$
isOk = false;
}
}
return isOk;
}
@PreDestroy
private void stopScheduledTasks() {
if (utilizationCacheUpdate != null) {
utilizationCacheUpdate.cancel(true);
}
if (inventoryCacheUpdate != null) {
inventoryCacheUpdate.cancel(true);
}
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException,
ServletException {
Dashboard dashboard;
response.setContentType(CONTENT_TYPE);
response.setCharacterEncoding(ENCODING);
try {
// Check if the browser wants a forced error, fake data, non-cached data or standard cached data
boolean preferFake = false;
boolean preferError = false;
boolean preferNoCache = false;
String preferHeader = request.getHeader(PREFER_HEADER);
String[] preferOptions = preferHeader == null ? new String[0] : preferHeader.trim().split("\\s*,\\s*"); //$NON-NLS-1$
for (String option : preferOptions) {
switch (option) {
case PREFER_FAKE_DATA:
preferFake = true;
break;
case PREFER_ERROR:
preferError = true;
break;
case PREFER_NO_CACHE:
preferNoCache = true;
break;
}
}
// Respond to the client based on the preferred method and state of servlet configuration
if (preferError) {
log.debug("client requested an error condition"); //$NON-NLS-1$
throw new ServletException("An error condition was requested."); //$NON-NLS-1$
} else if (preferFake) {
log.debug("client requested fake data"); //$NON-NLS-1$
dashboard = getFakeDashboard();
} else if (!dwhAvailable) {
log.debug("client request cannot be fulfilled, DWH is not available"); //$NON-NLS-1$
throw new ServletException("Dashboard data is not available."); //$NON-NLS-1$
} else if (preferNoCache) {
log.debug("client requested non-cache direct query data"); //$NON-NLS-1$
dashboard = getDashboard();
dashboard.setInventory(lookupInventory());
} else {
dashboard = getDashboardFromCache();
}
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), dashboard);
} catch (final DashboardDataException se) {
log.error("Unable to retrieve dashboard data", se); //$NON-NLS-1$
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(),
new DashboardError("Unable to retrieve dashboard data")); //$NON-NLS-1$
}
}
private Dashboard getDashboardFromCache() throws DashboardDataException {
Dashboard dashboard;
Inventory inventory;
synchronized (UTILIZATION_LOCK) {
// Get the dashboard from the cache if we can. If not, query the database.
dashboard = dashboardCache.get(UTILIZATION_KEY);
if (dashboard == null) {
dashboard = populateUtilizationCache();
}
// Inventory is in a different cache. Get the data from the cache if we can. If not, query
// the database. Since this is potentially modifying the dashboard object, we need to have
// this inside the synchronized block of the dashboard.
inventory = inventoryCache.get(INVENTORY_KEY);
if (inventory == null) {
inventory = populateInventoryCache();
}
}
dashboard.setInventory(inventory);
return dashboard;
}
private Dashboard populateUtilizationCache() throws DashboardDataException {
long startTime = System.currentTimeMillis();
Dashboard dashboard = getDashboard();
long endTime = System.currentTimeMillis();
if (enableBackgroundCacheUpdate) {
dashboardCache.put(UTILIZATION_KEY, dashboard);
} else {
dashboardCache.put(UTILIZATION_KEY, dashboard, UTILIZATION_CACHE_UPDATE_INTERVAL, TimeUnit.SECONDS);
}
log.debug("Dashboard utilization cache updated in {}ms", endTime-startTime); //$NON-NLS-1$
return dashboard;
}
private Inventory populateInventoryCache() throws DashboardDataException {
long startTime = System.currentTimeMillis();
Inventory inventory = lookupInventory();
long endTime = System.currentTimeMillis();
if (enableBackgroundCacheUpdate) {
inventoryCache.put(INVENTORY_KEY, inventory);
} else {
inventoryCache.put(INVENTORY_KEY, inventory, INVENTORY_CACHE_UPDATE_INTERVAL, TimeUnit.SECONDS);
}
log.debug("Dashboard inventoy cache updated in {}ms", endTime-startTime); //$NON-NLS-1$
return inventory;
}
private Dashboard getFakeDashboard() {
Random random = new Random();
Dashboard dashboard = new Dashboard();
dashboard.setGlobalUtilization(FakeDataGenerator.fakeGlobalUtilization(random));
dashboard.setHeatMapData(FakeDataGenerator.fakeHeatMapData(random));
dashboard.setInventory(FakeDataGenerator.fakeInventory(random));
return dashboard;
}
private Dashboard getDashboard() throws DashboardDataException {
Dashboard dashboard = new Dashboard();
dashboard.setGlobalUtilization(lookupGlobalUtilization());
dashboard.setHeatMapData(lookupClusterUtilization());
return dashboard;
}
private Inventory lookupInventory() throws DashboardDataException {
Inventory inventory = new Inventory();
inventory.setDc(InventoryHelper.getDcInventoryStatus(engineDataSource));
inventory.setCluster(InventoryHelper.getClusterInventoryStatus(engineDataSource));
inventory.setHost(InventoryHelper.getHostInventoryStatus(engineDataSource));
inventory.setStorage(InventoryHelper.getStorageInventoryStatus(engineDataSource));
inventory.setVm(InventoryHelper.getVmInventorySummary(engineDataSource));
inventory.setVolume(InventoryHelper.getGlusterVolumeInventorySummary(engineDataSource));
inventory.setEvent(EventHelper.getEventStatus(engineDataSource));
return inventory;
}
private HeatMapData lookupClusterUtilization() throws DashboardDataException {
HeatMapData utilization = new HeatMapData();
HeatMapHelper.getCpuAndMemory(utilization, dwhDataSource);
utilization.setStorage(HeatMapHelper.getStorage(dwhDataSource));
return utilization;
}
private GlobalUtilization lookupGlobalUtilization() throws DashboardDataException {
GlobalUtilization utilization = new GlobalUtilization();
HourlySummaryHelper.getCpuMemSummary(utilization, dwhDataSource);
utilization.setStorage(HourlySummaryHelper.getStorageSummary(dwhDataSource));
return utilization;
}
private static class DashboardError {
private String message;
DashboardError(String message) {
setMessage(message);
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
}