/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.ambari.server.orm.dao; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Order; import javax.persistence.metamodel.SingularAttribute; import org.apache.ambari.annotations.Experimental; import org.apache.ambari.annotations.ExperimentalFeature; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.api.query.JpaPredicateVisitor; import org.apache.ambari.server.api.query.JpaSortBuilder; import org.apache.ambari.server.cleanup.TimeBasedCleanupPolicy; import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.controller.AlertCurrentRequest; import org.apache.ambari.server.controller.AlertHistoryRequest; import org.apache.ambari.server.controller.spi.Predicate; import org.apache.ambari.server.controller.utilities.PredicateHelper; import org.apache.ambari.server.events.AggregateAlertRecalculateEvent; import org.apache.ambari.server.events.publishers.AlertEventPublisher; import org.apache.ambari.server.orm.RequiresSession; import org.apache.ambari.server.orm.entities.AlertCurrentEntity; import org.apache.ambari.server.orm.entities.AlertCurrentEntity_; import org.apache.ambari.server.orm.entities.AlertHistoryEntity; import org.apache.ambari.server.orm.entities.AlertHistoryEntity_; import org.apache.ambari.server.orm.entities.AlertNoticeEntity; import org.apache.ambari.server.state.AlertState; import org.apache.ambari.server.state.Cluster; import org.apache.ambari.server.state.Clusters; import org.apache.ambari.server.state.MaintenanceState; import org.apache.ambari.server.state.alert.Scope; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.persist.Transactional; /** * The {@link AlertsDAO} class manages the {@link AlertHistoryEntity} and * {@link AlertCurrentEntity} instances. Each {@link AlertHistoryEntity} is * known as an "alert" that has been triggered and received. * <p/> * If alert caching is enabled, then updates to {@link AlertCurrentEntity} are * not immediately persisted to JPA. Instead, they are kept in a cache and * periodically flushed. This means that many queries will need to swap in the * cached {@link AlertCurrentEntity} with that returned from the EclipseLink JPA * entity manager. */ @Singleton @Experimental(feature = ExperimentalFeature.ALERT_CACHING) public class AlertsDAO implements Cleanable { /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(AlertsDAO.class); /** * A template of JPQL for getting the number of hosts in various states. */ private static final String ALERT_COUNT_SQL_TEMPLATE = "SELECT NEW %s(" + "SUM(CASE WHEN history.alertState = :okState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :warningState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :criticalState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :unknownState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN alert.maintenanceState != :maintenanceStateOff THEN 1 ELSE 0 END)) " + "FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId"; private static final String ALERT_COUNT_PER_HOST_SQL_TEMPLATE = "SELECT NEW %s(" + "history.hostName, " + "SUM(CASE WHEN history.alertState = :okState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :warningState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :criticalState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN history.alertState = :unknownState AND alert.maintenanceState = :maintenanceStateOff THEN 1 ELSE 0 END), " + "SUM(CASE WHEN alert.maintenanceState != :maintenanceStateOff THEN 1 ELSE 0 END)) " + "FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId GROUP BY history.hostName"; /** * JPA entity manager */ @Inject private Provider<EntityManager> m_entityManagerProvider; /** * DAO utilities for dealing mostly with {@link TypedQuery} results. */ @Inject private DaoUtils m_daoUtils; /** * Publishes alert events when particular DAO methods are called. */ @Inject private AlertEventPublisher m_alertEventPublisher; /** * Used to lookup clusters. */ @Inject private Provider<Clusters> m_clusters; /** * Configuration. */ private final Configuration m_configuration; /** * A cache of current alert information. The {@link AlertCurrentEntity} * instances cached are currently managed. This allows the cached instances to * be easiler flushed from the cache to JPA. * <p/> * This also means that the cache is holding onto a rather large map of JPA * entities. This could lead to OOM errors over time if the indirectly * referenced entity map contains more than just {@link AlertCurrentEntity}. */ private LoadingCache<AlertCacheKey, AlertCurrentEntity> m_currentAlertCache = null; /** * Batch size to query the DB and use the results in an IN clause. */ private static final int BATCH_SIZE = 999; /** * Constructor. * */ @Inject public AlertsDAO(Configuration configuration) { m_configuration = configuration; if( m_configuration.isAlertCacheEnabled() ){ int maximumSize = m_configuration.getAlertCacheSize(); LOG.info("Alert caching is enabled (size={}, flushInterval={}m)", maximumSize, m_configuration.getAlertCacheFlushInterval()); // construct a cache for current alerts which will prevent database hits // on every heartbeat m_currentAlertCache = CacheBuilder.newBuilder().maximumSize( maximumSize).build(new CacheLoader<AlertCacheKey, AlertCurrentEntity>() { @Override public AlertCurrentEntity load(AlertCacheKey key) throws Exception { LOG.debug("Cache miss for alert key {}, fetching from JPA", key); final AlertCurrentEntity alertCurrentEntity; long clusterId = key.getClusterId(); String alertDefinitionName = key.getAlertDefinitionName(); String hostName = key.getHostName(); if (StringUtils.isEmpty(hostName)) { alertCurrentEntity = findCurrentByNameNoHostInternalInJPA(clusterId, alertDefinitionName); } else { alertCurrentEntity = findCurrentByHostAndNameInJPA(clusterId, hostName, alertDefinitionName); } if (null == alertCurrentEntity) { LOG.trace("Cache lookup failed for {} because the alert does not yet exist", key); throw new AlertNotYetCreatedException(); } return alertCurrentEntity; } }); } } /** * Gets an alert with the specified ID. * * @param alertId * the ID of the alert to retrieve. * @return the alert or {@code null} if none exists. */ @RequiresSession public AlertHistoryEntity findById(long alertId) { return m_entityManagerProvider.get().find(AlertHistoryEntity.class, alertId); } /** * Gets all alerts stored in the database across all clusters. * * @return all alerts or an empty list if none exist (never {@code null}). */ @RequiresSession public List<AlertHistoryEntity> findAll() { TypedQuery<AlertHistoryEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAll", AlertHistoryEntity.class); return m_daoUtils.selectList(query); } /** * Gets all alerts stored in the database for the given cluster. * * @param clusterId * the ID of the cluster. * @return all alerts in the specified cluster or an empty list if none exist * (never {@code null}). */ @RequiresSession public List<AlertHistoryEntity> findAll(long clusterId) { TypedQuery<AlertHistoryEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAllInCluster", AlertHistoryEntity.class); query.setParameter("clusterId", clusterId); return m_daoUtils.selectList(query); } /** * Gets all alerts stored in the database for the given cluster that have one * of the specified alert states. * * @param clusterId * the ID of the cluster. * @param alertStates * the states to match for the retrieved alerts (not {@code null}). * @return the alerts matching the specified states and cluster, or an empty * list if none. */ @RequiresSession public List<AlertHistoryEntity> findAll(long clusterId, List<AlertState> alertStates) { if (null == alertStates || alertStates.size() == 0) { return Collections.emptyList(); } TypedQuery<AlertHistoryEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAllInClusterWithState", AlertHistoryEntity.class); query.setParameter("clusterId", clusterId); query.setParameter("alertStates", alertStates); return m_daoUtils.selectList(query); } /** * Gets all alerts stored in the database for the given cluster and that fall * withing the specified date range. Dates are expected to be in milliseconds * since the epoch, normalized to UTC time. * * @param clusterId * the ID of the cluster. * @param startDate * the date that the earliest entry must occur after, normalized to * UTC, or {@code null} for all entries that occur before the given * end date. * @param endDate * the date that the latest entry must occur before, normalized to * UTC, or {@code null} for all entries that occur after the given * start date. * @return the alerts matching the specified date range. */ @RequiresSession public List<AlertHistoryEntity> findAll(long clusterId, Date startDate, Date endDate) { if (null == startDate && null == endDate) { return Collections.emptyList(); } TypedQuery<AlertHistoryEntity> query = null; if (null != startDate && null != endDate) { if (startDate.after(endDate)) { return Collections.emptyList(); } query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAllInClusterBetweenDates", AlertHistoryEntity.class); query.setParameter("clusterId", clusterId); query.setParameter("startDate", startDate.getTime()); query.setParameter("endDate", endDate.getTime()); } else if (null != startDate) { query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAllInClusterAfterDate", AlertHistoryEntity.class); query.setParameter("clusterId", clusterId); query.setParameter("afterDate", startDate.getTime()); } else if (null != endDate) { query = m_entityManagerProvider.get().createNamedQuery( "AlertHistoryEntity.findAllInClusterBeforeDate", AlertHistoryEntity.class); query.setParameter("clusterId", clusterId); query.setParameter("beforeDate", endDate.getTime()); } if (null == query) { return Collections.emptyList(); } return m_daoUtils.selectList(query); } /** * Finds all {@link AlertHistoryEntity} that match the provided * {@link AlertHistoryRequest}. This method will make JPA do the heavy lifting * of providing a slice of the result set. * * @param request * @return */ @RequiresSession public List<AlertHistoryEntity> findAll(AlertHistoryRequest request) { EntityManager entityManager = m_entityManagerProvider.get(); // convert the Ambari predicate into a JPA predicate HistoryPredicateVisitor visitor = new HistoryPredicateVisitor(); PredicateHelper.visit(request.Predicate, visitor); CriteriaQuery<AlertHistoryEntity> query = visitor.getCriteriaQuery(); javax.persistence.criteria.Predicate jpaPredicate = visitor.getJpaPredicate(); if (null != jpaPredicate) { query.where(jpaPredicate); } // sorting JpaSortBuilder<AlertHistoryEntity> sortBuilder = new JpaSortBuilder<>(); List<Order> sortOrders = sortBuilder.buildSortOrders(request.Sort, visitor); query.orderBy(sortOrders); // pagination TypedQuery<AlertHistoryEntity> typedQuery = entityManager.createQuery(query); if (null != request.Pagination) { typedQuery.setFirstResult(request.Pagination.getOffset()); typedQuery.setMaxResults(request.Pagination.getPageSize()); } return m_daoUtils.selectList(typedQuery); } /** * Finds all {@link AlertCurrentEntity} that match the provided * {@link AlertCurrentRequest}. This method will make JPA do the heavy lifting * of providing a slice of the result set. * * @param request * @return */ @Transactional public List<AlertCurrentEntity> findAll(AlertCurrentRequest request) { EntityManager entityManager = m_entityManagerProvider.get(); // convert the Ambari predicate into a JPA predicate CurrentPredicateVisitor visitor = new CurrentPredicateVisitor(); PredicateHelper.visit(request.Predicate, visitor); CriteriaQuery<AlertCurrentEntity> query = visitor.getCriteriaQuery(); javax.persistence.criteria.Predicate jpaPredicate = visitor.getJpaPredicate(); if (null != jpaPredicate) { query.where(jpaPredicate); } // sorting JpaSortBuilder<AlertCurrentEntity> sortBuilder = new JpaSortBuilder<>(); List<Order> sortOrders = sortBuilder.buildSortOrders(request.Sort, visitor); query.orderBy(sortOrders); // pagination TypedQuery<AlertCurrentEntity> typedQuery = entityManager.createQuery(query); if( null != request.Pagination ){ // prevent JPA errors when -1 is passed in by accident int offset = request.Pagination.getOffset(); if (offset < 0) { offset = 0; } typedQuery.setFirstResult(offset); typedQuery.setMaxResults(request.Pagination.getPageSize()); } List<AlertCurrentEntity> alerts = m_daoUtils.selectList(typedQuery); // if caching is enabled, replace results with cached values when present if (m_configuration.isAlertCacheEnabled()) { alerts = supplementWithCachedAlerts(alerts); } return alerts; } /** * Gets the total count of all {@link AlertHistoryEntity} rows that match the * specified {@link Predicate}. * * @param predicate * the predicate to apply, or {@code null} for none. * @return the total count of rows that would be returned in a result set. */ public int getCount(Predicate predicate) { return 0; } /** * Gets the current alerts. * * @return the current alerts or an empty list if none exist (never * {@code null}). */ @RequiresSession public List<AlertCurrentEntity> findCurrent() { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findAll", AlertCurrentEntity.class); List<AlertCurrentEntity> alerts = m_daoUtils.selectList(query); // if caching is enabled, replace results with cached values when present if (m_configuration.isAlertCacheEnabled()) { alerts = supplementWithCachedAlerts(alerts); } return alerts; } /** * Gets a current alert with the specified ID. * * @param alertId * the ID of the alert to retrieve. * @return the alert or {@code null} if none exists. */ @RequiresSession public AlertCurrentEntity findCurrentById(long alertId) { return m_entityManagerProvider.get().find(AlertCurrentEntity.class, alertId); } /** * Gets the current alerts for the specified definition ID. * * @param definitionId * the ID of the definition to retrieve current alerts for. * @return the current alerts for the definition or an empty list if none * exist (never {@code null}). */ @RequiresSession public List<AlertCurrentEntity> findCurrentByDefinitionId(long definitionId) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByDefinitionId", AlertCurrentEntity.class); query.setParameter("definitionId", Long.valueOf(definitionId)); List<AlertCurrentEntity> alerts = m_daoUtils.selectList(query); // if caching is enabled, replace results with cached values when present if (m_configuration.isAlertCacheEnabled()) { alerts = supplementWithCachedAlerts(alerts); } return alerts; } /** * Gets the current alerts for a given cluster. * * @return the current alerts for the given cluster or an empty list if none * exist (never {@code null}). */ @RequiresSession public List<AlertCurrentEntity> findCurrentByCluster(long clusterId) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByCluster", AlertCurrentEntity.class); query.setParameter("clusterId", Long.valueOf(clusterId)); List<AlertCurrentEntity> alerts = m_daoUtils.selectList(query); // if caching is enabled, replace results with cached values when present if (m_configuration.isAlertCacheEnabled()) { alerts = supplementWithCachedAlerts(alerts); } return alerts; } /** * Retrieves the summary information for a particular scope. The result is a * DTO since the columns are aggregated and don't fit to an entity. * * @param clusterId * the cluster id * @param serviceName * the service name. Use {@code null} to not filter on service. * @param hostName * the host name. Use {@code null} to not filter on host. * @return the summary DTO */ @RequiresSession public AlertSummaryDTO findCurrentCounts(long clusterId, String serviceName, String hostName) { String sql = String.format(ALERT_COUNT_SQL_TEMPLATE, AlertSummaryDTO.class.getName()); StringBuilder sb = new StringBuilder(sql); if (null != serviceName) { sb.append(" AND history.serviceName = :serviceName"); } if (null != hostName) { sb.append(" AND history.hostName = :hostName"); } TypedQuery<AlertSummaryDTO> query = m_entityManagerProvider.get().createQuery( sb.toString(), AlertSummaryDTO.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("okState", AlertState.OK); query.setParameter("warningState", AlertState.WARNING); query.setParameter("criticalState", AlertState.CRITICAL); query.setParameter("unknownState", AlertState.UNKNOWN); query.setParameter("maintenanceStateOff", MaintenanceState.OFF); if (null != serviceName) { query.setParameter("serviceName", serviceName); } if (null != hostName) { query.setParameter("hostName", hostName); } return m_daoUtils.selectSingle(query); } /** * Retrieves the summary information for all the hosts in the provided cluster. * The result is mapping from hostname to summary DTO. * * @param clusterId * the cluster id * @return map from hostnames to summary DTO */ @RequiresSession public Map<String, AlertSummaryDTO> findCurrentPerHostCounts(long clusterId) { String sql = String.format(ALERT_COUNT_PER_HOST_SQL_TEMPLATE, HostAlertSummaryDTO.class.getName()); StringBuilder sb = new StringBuilder(sql); TypedQuery<HostAlertSummaryDTO> query = m_entityManagerProvider.get().createQuery(sb.toString(), HostAlertSummaryDTO.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("okState", AlertState.OK); query.setParameter("warningState", AlertState.WARNING); query.setParameter("criticalState", AlertState.CRITICAL); query.setParameter("unknownState", AlertState.UNKNOWN); query.setParameter("maintenanceStateOff", MaintenanceState.OFF); Map<String, AlertSummaryDTO> map = new HashMap<>(); List<HostAlertSummaryDTO> resultList = m_daoUtils.selectList(query); for (HostAlertSummaryDTO result : resultList) { map.put(result.getHostName(), result); } return map; } /** * Retrieve the summary alert information for all hosts. This is different * from {@link #findCurrentCounts(long, String, String)} since this will * return only alerts related to hosts and those values will be the total * number of hosts affected, not the total number of alerts. * * @param clusterId * the cluster id * @return the summary DTO for host alerts. */ @RequiresSession public AlertHostSummaryDTO findCurrentHostCounts(long clusterId) { String sql = String.format(ALERT_COUNT_PER_HOST_SQL_TEMPLATE, HostAlertSummaryDTO.class.getName()); StringBuilder sb = new StringBuilder(sql); TypedQuery<HostAlertSummaryDTO> query = m_entityManagerProvider.get().createQuery(sb.toString(), HostAlertSummaryDTO.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("okState", AlertState.OK); query.setParameter("criticalState", AlertState.CRITICAL); query.setParameter("warningState", AlertState.WARNING); query.setParameter("unknownState", AlertState.UNKNOWN); query.setParameter("maintenanceStateOff", MaintenanceState.OFF); int okCount = 0; int warningCount = 0; int criticalCount = 0; int unknownCount = 0; List<HostAlertSummaryDTO> resultList = m_daoUtils.selectList(query); for (HostAlertSummaryDTO result : resultList) { if (result.getHostName() == null) { continue; } if (result.getCriticalCount() > 0) { criticalCount++; } else if (result.getWarningCount() > 0) { warningCount++; } else if (result.getUnknownCount() > 0) { unknownCount++; } else { okCount++; } } AlertHostSummaryDTO hostSummary = new AlertHostSummaryDTO(okCount, unknownCount, warningCount, criticalCount); return hostSummary; } /** * Gets the current alerts for a given service. * * @return the current alerts for the given service or an empty list if none * exist (never {@code null}). */ @RequiresSession public List<AlertCurrentEntity> findCurrentByService(long clusterId, String serviceName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByService", AlertCurrentEntity.class); query.setParameter("clusterId", clusterId); query.setParameter("serviceName", serviceName); query.setParameter("inlist", EnumSet.of(Scope.ANY, Scope.SERVICE)); List<AlertCurrentEntity> alerts = m_daoUtils.selectList(query); // if caching is enabled, replace results with cached values when present if (m_configuration.isAlertCacheEnabled()) { alerts = supplementWithCachedAlerts(alerts); } return alerts; } /** * Locate the current alert for the provided service and alert name. This * method will first consult the cache if configured with * {@link Configuration#isAlertCacheEnabled()}. * * @param clusterId * the cluster id * @param hostName * the name of the host (not {@code null}). * @param alertName * the name of the alert (not {@code null}). * @return the current record, or {@code null} if not found */ public AlertCurrentEntity findCurrentByHostAndName(long clusterId, String hostName, String alertName) { if( m_configuration.isAlertCacheEnabled() ){ AlertCacheKey key = new AlertCacheKey(clusterId, alertName, hostName); try { return m_currentAlertCache.get(key); } catch (ExecutionException executionException) { Throwable cause = executionException.getCause(); if (!(cause instanceof AlertNotYetCreatedException)) { LOG.warn("Unable to retrieve alert for key {} from the cache", key); } } } return findCurrentByHostAndNameInJPA(clusterId, hostName, alertName); } /** * Locate the current alert for the provided service and alert name. * * @param clusterId * the cluster id * @param hostName * the name of the host (not {@code null}). * @param alertName * the name of the alert (not {@code null}). * @return the current record, or {@code null} if not found */ @RequiresSession private AlertCurrentEntity findCurrentByHostAndNameInJPA(long clusterId, String hostName, String alertName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByHostAndName", AlertCurrentEntity.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("hostName", hostName); query.setParameter("definitionName", alertName); return m_daoUtils.selectOne(query); } /** * Removes alert history and current alerts for the specified alert defintiion * ID. This will invoke {@link EntityManager#clear()} when completed since the * JPQL statement will remove entries without going through the EM. * * @param definitionId * the ID of the definition to remove. */ @Transactional public void removeByDefinitionId(long definitionId) { EntityManager entityManager = m_entityManagerProvider.get(); TypedQuery<AlertCurrentEntity> currentQuery = entityManager.createNamedQuery( "AlertCurrentEntity.removeByDefinitionId", AlertCurrentEntity.class); currentQuery.setParameter("definitionId", definitionId); currentQuery.executeUpdate(); TypedQuery<AlertHistoryEntity> historyQuery = entityManager.createNamedQuery( "AlertHistoryEntity.removeByDefinitionId", AlertHistoryEntity.class); historyQuery.setParameter("definitionId", definitionId); historyQuery.executeUpdate(); entityManager.clear(); // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } } /** * Remove a current alert whose history entry matches the specfied ID. * * @param historyId the ID of the history entry. * @return the number of alerts removed. */ @Transactional public int removeCurrentByHistoryId(long historyId) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.removeByHistoryId", AlertCurrentEntity.class); query.setParameter("historyId", historyId); int rowsRemoved = query.executeUpdate(); // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } return rowsRemoved; } /** * Remove all current alerts that are disabled. * * @return the number of alerts removed. */ @Transactional public int removeCurrentDisabledAlerts() { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findDisabled", AlertCurrentEntity.class); int rowsRemoved = 0; List<AlertCurrentEntity> currentEntities = m_daoUtils.selectList(query); if (currentEntities != null) { for (AlertCurrentEntity currentEntity : currentEntities) { remove(currentEntity); rowsRemoved++; } } // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } return rowsRemoved; } /** * Remove the current alert that matches the given service. This is used in * cases where the service was removed from the cluster. * <p> * This method will also fire an {@link AggregateAlertRecalculateEvent} in * order to recalculate all aggregates. * * @param clusterId * the ID of the cluster. * @param serviceName * the name of the service that the current alerts are being removed * for (not {@code null}). * @return the number of alerts removed. */ @Transactional public int removeCurrentByService(long clusterId, String serviceName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByServiceName", AlertCurrentEntity.class); query.setParameter("serviceName", serviceName); int removedItems = 0; List<AlertCurrentEntity> currentEntities = m_daoUtils.selectList(query); if (currentEntities != null) { for (AlertCurrentEntity currentEntity : currentEntities) { remove(currentEntity); removedItems++; } } // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } // publish the event to recalculate aggregates m_alertEventPublisher.publish(new AggregateAlertRecalculateEvent(clusterId)); return removedItems; } /** * Remove the current alert that matches the given host. This is used in cases * where the host was removed from the cluster. * <p> * This method will also fire an {@link AggregateAlertRecalculateEvent} in * order to recalculate all aggregates. * * @param hostName * the name of the host that the current alerts are being removed for * (not {@code null}). * @return the number of alerts removed. */ @Transactional public int removeCurrentByHost(String hostName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByHost", AlertCurrentEntity.class); query.setParameter("hostName", hostName); List<AlertCurrentEntity> currentEntities = m_daoUtils.selectList(query); int removedItems = 0; if (currentEntities != null) { for (AlertCurrentEntity currentEntity : currentEntities) { remove(currentEntity); removedItems++; } } // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } // publish the event to recalculate aggregates for every cluster since a host could potentially have several clusters try { Map<String, Cluster> clusters = m_clusters.get().getClusters(); for (Map.Entry<String, Cluster> entry : clusters.entrySet()) { m_alertEventPublisher.publish(new AggregateAlertRecalculateEvent( entry.getValue().getClusterId())); } } catch (Exception ambariException) { LOG.warn("Unable to recalcuate aggregate alerts after removing host {}", hostName); } return removedItems; } /** * Remove the current alert that matches the given service, component and * host. This is used in cases where the component was removed from the host. * <p> * This method will also fire an {@link AggregateAlertRecalculateEvent} in * order to recalculate all aggregates. * * @param clusterId * the ID of the cluster. * @param serviceName * the name of the service that the current alerts are being removed * for (not {@code null}). * @param componentName * the name of the component that the current alerts are being * removed for (not {@code null}). * @param hostName * the name of the host that the current alerts are being removed for * (not {@code null}). * @return the number of alerts removed. */ @Transactional public int removeCurrentByServiceComponentHost(long clusterId, String serviceName, String componentName, String hostName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByHostComponent", AlertCurrentEntity.class); query.setParameter("serviceName", serviceName); query.setParameter("componentName", componentName); query.setParameter("hostName", hostName); List<AlertCurrentEntity> currentEntities = m_daoUtils.selectList(query); int removedItems = 0; if (currentEntities != null) { for (AlertCurrentEntity currentEntity : currentEntities) { remove(currentEntity); removedItems++; } } // if caching is enabled, invalidate the cache to force the latest values // back from the DB if (m_configuration.isAlertCacheEnabled()) { m_currentAlertCache.invalidateAll(); } // publish the event to recalculate aggregates m_alertEventPublisher.publish(new AggregateAlertRecalculateEvent(clusterId)); return removedItems; } /** * Persists a new alert. * * @param alert * the alert to persist (not {@code null}). */ @Transactional public void create(AlertHistoryEntity alert) { m_entityManagerProvider.get().persist(alert); } /** * Refresh the state of the alert from the database. * * @param alert * the alert to refresh (not {@code null}). */ @Transactional public void refresh(AlertHistoryEntity alert) { m_entityManagerProvider.get().refresh(alert); } /** * Merge the speicified alert with the existing alert in the database. * * @param alert * the alert to merge (not {@code null}). * @return the updated alert with merged content (never {@code null}). */ @Transactional public AlertHistoryEntity merge(AlertHistoryEntity alert) { return m_entityManagerProvider.get().merge(alert); } /** * Removes the specified alert from the database. * * @param alert * the alert to remove. */ @Transactional public void remove(AlertHistoryEntity alert) { alert = merge(alert); removeCurrentByHistoryId(alert.getAlertId()); m_entityManagerProvider.get().remove(alert); } /** * Persists a new current alert. * * @param alert * the current alert to persist (not {@code null}). */ @Transactional public void create(AlertCurrentEntity alert) { m_entityManagerProvider.get().persist(alert); } /** * Refresh the state of the current alert from the database. * * @param alert * the current alert to refresh (not {@code null}). */ @Transactional public void refresh(AlertCurrentEntity alert) { m_entityManagerProvider.get().refresh(alert); } /** * Merge the speicified current alert with the existing alert in the database. * * @param alert * the current alert to merge (not {@code null}). * @return the updated current alert with merged content (never {@code null}). */ @Transactional public AlertCurrentEntity merge(AlertCurrentEntity alert) { // perform the JPA merge alert = m_entityManagerProvider.get().merge(alert); // if caching is enabled, update the cache if( m_configuration.isAlertCacheEnabled() ){ AlertCacheKey key = AlertCacheKey.build(alert); m_currentAlertCache.put(key, alert); } return alert; } /** * Updates the internal cache of alerts with the specified alert. Unlike * {@link #merge(AlertCurrentEntity)}, this is not transactional and only * updates the cache. * <p/> * The alert should already exist in JPA - this is mainly to update the text * and timestamp. * * @param alert * the alert to update in the cache (not {@code null}). * @param updateCacheOnly * if {@code true}, then only the cache is updated and not JPA. * @see Configuration#isAlertCacheEnabled() */ public AlertCurrentEntity merge(AlertCurrentEntity alert, boolean updateCacheOnly) { // cache only updates if (updateCacheOnly) { AlertCacheKey key = AlertCacheKey.build(alert); // cache not configured, log error if (!m_configuration.isAlertCacheEnabled()) { LOG.error( "Unable to update a cached alert instance for {} because cached alerts are not enabled", key); } else { // update cache and return alert; no database work m_currentAlertCache.put(key, alert); return alert; } } return merge(alert); } /** * Removes the specified current alert from the database. * * @param alert * the current alert to remove. */ @Transactional public void remove(AlertCurrentEntity alert) { m_entityManagerProvider.get().remove(merge(alert)); } /** * Finds the aggregate counts for an alert name, across all hosts. * @param clusterId the cluster id * @param alertName the name of the alert to find the aggregate * @return the summary data */ @RequiresSession public AlertSummaryDTO findAggregateCounts(long clusterId, String alertName) { String sql = String.format(ALERT_COUNT_SQL_TEMPLATE, AlertSummaryDTO.class.getName()); StringBuilder buffer = new StringBuilder(sql); buffer.append(" AND history.alertDefinition.definitionName = :definitionName"); TypedQuery<AlertSummaryDTO> query = m_entityManagerProvider.get().createQuery( buffer.toString(), AlertSummaryDTO.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("okState", AlertState.OK); query.setParameter("warningState", AlertState.WARNING); query.setParameter("criticalState", AlertState.CRITICAL); query.setParameter("unknownState", AlertState.UNKNOWN); query.setParameter("maintenanceStateOff", MaintenanceState.OFF); query.setParameter("definitionName", alertName); return m_daoUtils.selectSingle(query); } /** * Locate the current alert for the provided service and alert name, but when * host is not set ({@code IS NULL}). This method will first consult the cache * if configured with {@link Configuration#isAlertCacheEnabled()}. * * @param clusterId * the cluster id * @param alertName * the name of the alert * @return the current record, or {@code null} if not found */ public AlertCurrentEntity findCurrentByNameNoHost(long clusterId, String alertName) { if( m_configuration.isAlertCacheEnabled() ){ AlertCacheKey key = new AlertCacheKey(clusterId, alertName); try { return m_currentAlertCache.get(key); } catch (ExecutionException executionException) { Throwable cause = executionException.getCause(); if (!(cause instanceof AlertNotYetCreatedException)) { LOG.warn("Unable to retrieve alert for key {} from, the cache", key); } } } return findCurrentByNameNoHostInternalInJPA(clusterId, alertName); } /** * Locate the current alert for the provided service and alert name, but when * host is not set ({@code IS NULL}). This method * * @param clusterId * the cluster id * @param alertName * the name of the alert * @return the current record, or {@code null} if not found */ @RequiresSession private AlertCurrentEntity findCurrentByNameNoHostInternalInJPA(long clusterId, String alertName) { TypedQuery<AlertCurrentEntity> query = m_entityManagerProvider.get().createNamedQuery( "AlertCurrentEntity.findByNameAndNoHost", AlertCurrentEntity.class); query.setParameter("clusterId", Long.valueOf(clusterId)); query.setParameter("definitionName", alertName); return m_daoUtils.selectOne(query); } /** * Writes all cached {@link AlertCurrentEntity} instances to the database and * clears the cache. */ @Transactional public void flushCachedEntitiesToJPA() { if (!m_configuration.isAlertCacheEnabled()) { LOG.warn("Unable to flush cached alerts to JPA because caching is not enabled"); return; } // capture for logging purposes long cachedEntityCount = m_currentAlertCache.size(); ConcurrentMap<AlertCacheKey, AlertCurrentEntity> map = m_currentAlertCache.asMap(); Set<Entry<AlertCacheKey, AlertCurrentEntity>> entries = map.entrySet(); for (Entry<AlertCacheKey, AlertCurrentEntity> entry : entries) { merge(entry.getValue()); } m_currentAlertCache.invalidateAll(); LOG.info("Flushed {} cached alerts to the database", cachedEntityCount); } /** * Gets a list that is comprised of the original values replaced by any cached * values from {@link #m_currentAlertCache}. This method should only be * invoked if {@link Configuration#isAlertCacheEnabled()} is {@code true} * * @param alerts * the list of alerts to iterate over and replace with cached * instances. * @return the list of alerts from JPA combined with any cached alerts. */ private List<AlertCurrentEntity> supplementWithCachedAlerts(List<AlertCurrentEntity> alerts) { List<AlertCurrentEntity> cachedAlerts = new ArrayList<>(alerts.size()); for (AlertCurrentEntity alert : alerts) { AlertCacheKey key = AlertCacheKey.build(alert); AlertCurrentEntity cachedEntity = m_currentAlertCache.getIfPresent(key); if (null != cachedEntity) { alert = cachedEntity; } cachedAlerts.add(alert); } return cachedAlerts; } @Transactional @Override public long cleanup(TimeBasedCleanupPolicy policy) { long affectedRows = 0; Long clusterId = null; try { clusterId = m_clusters.get().getCluster(policy.getClusterName()).getClusterId(); affectedRows += cleanAlertNoticesForClusterBeforeDate(clusterId, policy.getToDateInMillis()); affectedRows += cleanAlertCurrentsForClusterBeforeDate(clusterId, policy.getToDateInMillis()); affectedRows += cleanAlertHistoriesForClusterBeforeDate(clusterId, policy.getToDateInMillis()); } catch (AmbariException e) { LOG.error("Error while looking up cluster with name: {}", policy.getClusterName(), e); throw new IllegalStateException(e); } return affectedRows; } /** * The {@link HistoryPredicateVisitor} is used to convert an Ambari * {@link Predicate} into a JPA {@link javax.persistence.criteria.Predicate}. */ private final class HistoryPredicateVisitor extends JpaPredicateVisitor<AlertHistoryEntity> { /** * Constructor. * */ public HistoryPredicateVisitor() { super(m_entityManagerProvider.get(), AlertHistoryEntity.class); } /** * {@inheritDoc} */ @Override public Class<AlertHistoryEntity> getEntityClass() { return AlertHistoryEntity.class; } /** * {@inheritDoc} */ @Override public List<? extends SingularAttribute<?, ?>> getPredicateMapping( String propertyId) { return AlertHistoryEntity_.getPredicateMapping().get(propertyId); } } /** * The {@link CurrentPredicateVisitor} is used to convert an Ambari * {@link Predicate} into a JPA {@link javax.persistence.criteria.Predicate}. */ private final class CurrentPredicateVisitor extends JpaPredicateVisitor<AlertCurrentEntity> { /** * Constructor. * */ public CurrentPredicateVisitor() { super(m_entityManagerProvider.get(), AlertCurrentEntity.class); } /** * {@inheritDoc} */ @Override public Class<AlertCurrentEntity> getEntityClass() { return AlertCurrentEntity.class; } /** * {@inheritDoc} */ @Override public List<? extends SingularAttribute<?, ?>> getPredicateMapping( String propertyId) { return AlertCurrentEntity_.getPredicateMapping().get(propertyId); } } /** * The {@link AlertCacheKey} class is used as a key in the cache of * {@link AlertCurrentEntity}. */ private final static class AlertCacheKey { private final long m_clusterId; private final String m_hostName; private final String m_alertDefinitionName; /** * Constructor. * * @param clusterId * @param alertDefinitionName */ private AlertCacheKey(long clusterId, String alertDefinitionName) { this(clusterId, alertDefinitionName, null); } /** * Constructor. * * @param clusterId * @param alertDefinitionName * @param hostName */ private AlertCacheKey(long clusterId, String alertDefinitionName, String hostName) { m_clusterId = clusterId; m_alertDefinitionName = alertDefinitionName; m_hostName = hostName; } /** * Builds a key from an entity. * * @param current * the entity to create the key for. * @return the key (never {@code null}). */ public static AlertCacheKey build(AlertCurrentEntity current) { AlertHistoryEntity history = current.getAlertHistory(); AlertCacheKey key = new AlertCacheKey(history.getClusterId(), history.getAlertDefinition().getDefinitionName(), history.getHostName()); return key; } /** * Gets the ID of the cluster that the alert is for. * * @return the clusterId */ public long getClusterId() { return m_clusterId; } /** * Gets the host name, or {@code null} if none. * * @return the hostName, or {@code null} if none. */ public String getHostName() { return m_hostName; } /** * Gets the unique name of the alert definition. * * @return the alertDefinitionName */ public String getAlertDefinitionName() { return m_alertDefinitionName; } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((m_alertDefinitionName == null) ? 0 : m_alertDefinitionName.hashCode()); result = prime * result + (int) (m_clusterId ^ (m_clusterId >>> 32)); result = prime * result + ((m_hostName == null) ? 0 : m_hostName.hashCode()); return result; } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } AlertCacheKey other = (AlertCacheKey) obj; if (m_clusterId != other.m_clusterId) { return false; } if (m_alertDefinitionName == null) { if (other.m_alertDefinitionName != null) { return false; } } else if (!m_alertDefinitionName.equals(other.m_alertDefinitionName)) { return false; } if (m_hostName == null) { if (other.m_hostName != null) { return false; } } else if (!m_hostName.equals(other.m_hostName)) { return false; } return true; } /** * {@inheritDoc} */ @Override public String toString() { StringBuilder buffer = new StringBuilder("AlertCacheKey{"); buffer.append("cluserId=").append(m_clusterId); buffer.append(", alertName=").append(m_alertDefinitionName); if (null != m_hostName) { buffer.append(", hostName=").append(m_hostName); } buffer.append("}"); return buffer.toString(); } } /** * The {@link AlertNotYetCreatedException} is used as a way to signal to the * {@link CacheLoader} that there is no value for the specified * {@link AlertCacheKey}. Because this cache doesn't understand {@code null} * values, we use the exception mechanism to indicate that it should be * created and that the {@code null} value should not be cached. */ @SuppressWarnings("serial") private static final class AlertNotYetCreatedException extends Exception { } /** * Find all @AlertHistoryEntity with date before provided date. * @param clusterId cluster id * @param beforeDateMillis timestamp in millis * @return List<Integer> ids */ private List<Integer> findAllAlertHistoryIdsBeforeDate(Long clusterId, long beforeDateMillis) { EntityManager entityManager = m_entityManagerProvider.get(); TypedQuery<Integer> alertHistoryQuery = entityManager.createNamedQuery("AlertHistoryEntity.findAllIdsInClusterBeforeDate", Integer.class); alertHistoryQuery.setParameter("clusterId", clusterId); alertHistoryQuery.setParameter("beforeDate", beforeDateMillis); return m_daoUtils.selectList(alertHistoryQuery); } /** * Deletes AlertNotice records in relation with AlertHistory entries older than the given date. * * @param clusterId the identifier of the cluster the AlertNotices belong to * @param beforeDateMillis the date in milliseconds the * @return a long representing the number of affected (deleted) records */ @Transactional int cleanAlertNoticesForClusterBeforeDate(Long clusterId, long beforeDateMillis) { LOG.info("Deleting AlertNotice entities before date " + new Date(beforeDateMillis)); EntityManager entityManager = m_entityManagerProvider.get(); List<Integer> ids = findAllAlertHistoryIdsBeforeDate(clusterId, beforeDateMillis); int affectedRows = 0; // Batch delete TypedQuery<AlertNoticeEntity> noticeQuery = entityManager.createNamedQuery("AlertNoticeEntity.removeByHistoryIds", AlertNoticeEntity.class); if (ids != null && !ids.isEmpty()) { for (int i = 0; i < ids.size(); i += BATCH_SIZE) { int endIndex = (i + BATCH_SIZE) > ids.size() ? ids.size() : (i + BATCH_SIZE); List<Integer> idsSubList = ids.subList(i, endIndex); LOG.info("Deleting AlertNotice entity batch with history ids: " + idsSubList.get(0) + " - " + idsSubList.get(idsSubList.size() - 1)); noticeQuery.setParameter("historyIds", idsSubList); affectedRows += noticeQuery.executeUpdate(); } } return affectedRows; } /** * Deletes AlertCurrent records in relation with AlertHistory entries older than the given date. * * @param clusterId the identifier of the cluster the AlertCurrents belong to * @param beforeDateMillis the date in milliseconds the * @return a long representing the number of affected (deleted) records */ @Transactional int cleanAlertCurrentsForClusterBeforeDate(long clusterId, long beforeDateMillis) { LOG.info("Deleting AlertCurrent entities before date " + new Date(beforeDateMillis)); EntityManager entityManager = m_entityManagerProvider.get(); List<Integer> ids = findAllAlertHistoryIdsBeforeDate(clusterId, beforeDateMillis); int affectedRows = 0; TypedQuery<AlertCurrentEntity> currentQuery = entityManager.createNamedQuery("AlertCurrentEntity.removeByHistoryIds", AlertCurrentEntity.class); if (ids != null && !ids.isEmpty()) { for (int i = 0; i < ids.size(); i += BATCH_SIZE) { int endIndex = (i + BATCH_SIZE) > ids.size() ? ids.size() : (i + BATCH_SIZE); List<Integer> idsSubList = ids.subList(i, endIndex); LOG.info("Deleting AlertCurrent entity batch with history ids: " + idsSubList.get(0) + " - " + idsSubList.get(idsSubList.size() - 1)); currentQuery.setParameter("historyIds", ids.subList(i, endIndex)); affectedRows += currentQuery.executeUpdate(); } } return affectedRows; } /** * Deletes AlertHistory entries in a cluster older than the given date. * * @param clusterId the identifier of the cluster the AlertHistory entries belong to * @param beforeDateMillis the date in milliseconds the * @return a long representing the number of affected (deleted) records */ @Transactional int cleanAlertHistoriesForClusterBeforeDate(Long clusterId, long beforeDateMillis) { return executeQuery("AlertHistoryEntity.removeInClusterBeforeDate", AlertHistoryEntity.class, clusterId, beforeDateMillis); } /** * Utility method for executing update or delete named queries having as input parameters the cluster id and a timestamp. * * @param namedQuery the named query to be executed * @param entityType the type of the entity * @param clusterId the cluster identifier * @param timestamp timestamp * @return the number of rows affected by the query execution. */ private int executeQuery(String namedQuery, Class entityType, long clusterId, long timestamp) { LOG.info("Starting: Delete/update entries older than [ {} ] for entity [{}]", timestamp, entityType); TypedQuery query = m_entityManagerProvider.get().createNamedQuery(namedQuery, entityType); query.setParameter("clusterId", clusterId); query.setParameter("beforeDate", timestamp); int affectedRows = query.executeUpdate(); m_entityManagerProvider.get().flush(); m_entityManagerProvider.get().clear(); LOG.info("Completed: Delete/update entries older than [ {} ] for entity: [{}]. Number of entities deleted: [{}]", timestamp, entityType, affectedRows); return affectedRows; } }