/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.server.measurement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.util.List; import java.util.Set; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jetbrains.annotations.NotNull; import org.rhq.core.db.DatabaseType; import org.rhq.core.db.DatabaseTypeFactory; import org.rhq.core.db.H2DatabaseType; import org.rhq.core.db.OracleDatabaseType; import org.rhq.core.db.Postgresql83DatabaseType; import org.rhq.core.db.PostgresqlDatabaseType; import org.rhq.core.db.SQLServerDatabaseType; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.common.EntityContext; import org.rhq.core.domain.criteria.CallTimeDataCriteria; import org.rhq.core.domain.measurement.MeasurementSchedule; import org.rhq.core.domain.measurement.calltime.CallTimeData; import org.rhq.core.domain.measurement.calltime.CallTimeDataComposite; import org.rhq.core.domain.measurement.calltime.CallTimeDataKey; import org.rhq.core.domain.measurement.calltime.CallTimeDataValue; import org.rhq.core.domain.server.PersistenceUtility; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.core.domain.util.PageOrdering; import org.rhq.core.util.jdbc.JDBCUtil; import org.rhq.enterprise.server.RHQConstants; import org.rhq.enterprise.server.alert.engine.AlertConditionCacheManagerLocal; import org.rhq.enterprise.server.alert.engine.AlertConditionCacheStats; import org.rhq.enterprise.server.authz.AuthorizationManagerLocal; import org.rhq.enterprise.server.authz.PermissionException; import org.rhq.enterprise.server.measurement.instrumentation.MeasurementMonitor; import org.rhq.enterprise.server.util.CriteriaQueryGenerator; import org.rhq.enterprise.server.util.CriteriaQueryRunner; /** * The manager for call-time metric data. * * @author Ian Springer */ @Stateless public class CallTimeDataManagerBean implements CallTimeDataManagerLocal, CallTimeDataManagerRemote { private static final String DATA_VALUE_TABLE_NAME = "RHQ_CALLTIME_DATA_VALUE"; private static final String DATA_KEY_TABLE_NAME = "RHQ_CALLTIME_DATA_KEY"; private static final String CALLTIME_KEY_INSERT_STATEMENT = "INSERT INTO " + DATA_KEY_TABLE_NAME + "(id, schedule_id, call_destination) " + "SELECT %s, ?, ? FROM RHQ_numbers WHERE i = 42 " + "AND NOT EXISTS (SELECT * FROM " + DATA_KEY_TABLE_NAME + " WHERE schedule_id = ? AND call_destination = ?)"; private static final String CALLTIME_KEY_INSERT_STATEMENT_AUTOINC = "INSERT INTO " + DATA_KEY_TABLE_NAME + "(schedule_id, call_destination) " + "SELECT ?, ? FROM RHQ_numbers WHERE i = 42 " + "AND NOT EXISTS (SELECT * FROM " + DATA_KEY_TABLE_NAME + " WHERE schedule_id = ? AND call_destination = ?)"; private static final String CALLTIME_VALUE_INSERT_STATEMENT = "INSERT /*+ APPEND */ INTO " + DATA_VALUE_TABLE_NAME + "(id, key_id, begin_time, end_time, minimum, maximum, total, count) " + "SELECT %s, key.id, ?, ?, ?, ?, ?, ? FROM " + DATA_KEY_TABLE_NAME + " key WHERE key.schedule_id = ? AND key.call_destination = ?"; private static final String CALLTIME_VALUE_INSERT_STATEMENT_AUTOINC = "INSERT INTO " + DATA_VALUE_TABLE_NAME + "(key_id, begin_time, end_time, minimum, maximum, total, count) SELECT key.id, ?, ?, ?, ?, ?, ? FROM " + DATA_KEY_TABLE_NAME + " key WHERE key.schedule_id = ? AND key.call_destination = ?"; private final Log log = LogFactory.getLog(CallTimeDataManagerBean.class); @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME) private EntityManager entityManager; @javax.annotation.Resource(name = "RHQ_DS", mappedName = RHQConstants.DATASOURCE_JNDI_NAME) private DataSource rhqDs; @EJB private AuthorizationManagerLocal authorizationManager; @EJB private CallTimeDataManagerLocal callTimeDataManager; @EJB private AlertConditionCacheManagerLocal alertConditionCacheManager; @Override @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public void addCallTimeData(@NotNull Set<CallTimeData> callTimeDataSet) { if (callTimeDataSet.isEmpty()) { return; } log.debug("Persisting call-time data for " + callTimeDataSet.size() + " schedules..."); long startTime = System.currentTimeMillis(); // First make sure a single row exists in the key table for each reported call destination. callTimeDataManager.insertCallTimeDataKeys(callTimeDataSet); // Finally, add the stats themselves to the value table. callTimeDataManager.insertCallTimeDataValues(callTimeDataSet); MeasurementMonitor.getMBean().incrementCallTimeInsertTime(System.currentTimeMillis() - startTime); } @Override public PageList<CallTimeDataComposite> findCallTimeDataRawForResource(Subject subject, int scheduleId, long beginTime, long endTime, PageControl pageControl) { pageControl.initDefaultOrderingField("value.beginTime", PageOrdering.ASC); MeasurementSchedule schedule = entityManager.find(MeasurementSchedule.class, scheduleId); int resourceId = schedule.getResource().getId(); if (authorizationManager.canViewResource(subject, resourceId) == false) { throw new PermissionException("User [" + subject + "] does not have permission to view call time data for measurementSchedule[id=" + scheduleId + "] and resource[id=" + resourceId + "]"); } String query = CallTimeDataValue.QUERY_FIND_RAW_FOR_RESOURCE; Query queryWithOrderBy = PersistenceUtility.createQueryWithOrderBy(entityManager, query, pageControl); Query queryCount = PersistenceUtility.createCountQuery(this.entityManager, query); queryWithOrderBy.setParameter("scheduleId", scheduleId); queryWithOrderBy.setParameter("beginTime", beginTime); queryWithOrderBy.setParameter("endTime", endTime); queryCount.setParameter("scheduleId", scheduleId); queryCount.setParameter("beginTime", beginTime); queryCount.setParameter("endTime", endTime); @SuppressWarnings("unchecked") List<CallTimeDataComposite> results = queryWithOrderBy.getResultList(); long count = (Long) queryCount.getSingleResult(); return new PageList<CallTimeDataComposite>(results, (int) count, pageControl); } @Override @SuppressWarnings("unchecked") public PageList<CallTimeDataComposite> findCallTimeDataForResource(Subject subject, int scheduleId, long beginTime, long endTime, PageControl pageControl) { pageControl.initDefaultOrderingField("SUM(value.total)/SUM(value.count)", PageOrdering.DESC); // only set if no ordering yet specified pageControl.addDefaultOrderingField("key.callDestination", PageOrdering.ASC); // add this to sort, if not already specified MeasurementSchedule schedule = entityManager.find(MeasurementSchedule.class, scheduleId); int resourceId = schedule.getResource().getId(); if (authorizationManager.canViewResource(subject, resourceId) == false) { throw new PermissionException("User [" + subject + "] does not have permission to view call time data for measurementSchedule[id=" + scheduleId + "] and resource[id=" + resourceId + "]"); } String query = CallTimeDataValue.QUERY_FIND_COMPOSITES_FOR_RESOURCE; Query queryWithOrderBy = PersistenceUtility.createQueryWithOrderBy(entityManager, query, pageControl); Query queryCount = PersistenceUtility.createCountQuery(this.entityManager, query); queryWithOrderBy.setParameter("scheduleId", scheduleId); queryWithOrderBy.setParameter("beginTime", beginTime); queryWithOrderBy.setParameter("endTime", endTime); List<CallTimeDataComposite> results = queryWithOrderBy.getResultList(); queryCount.setParameter("scheduleId", scheduleId); queryCount.setParameter("beginTime", beginTime); queryCount.setParameter("endTime", endTime); // Because of the use of the GROUP BY clause, the result list count will be returned as // the number of rows, rather than as a single number. long count = queryCount.getResultList().size(); return new PageList<CallTimeDataComposite>(results, (int) count, pageControl); } @Override public PageList<CallTimeDataComposite> findCallTimeDataForCompatibleGroup(Subject subject, int groupId, long beginTime, long endTime, PageControl pageControl) { return findCallTimeDataForContext(subject, EntityContext.forGroup(groupId), beginTime, endTime, null, pageControl); } @Override public PageList<CallTimeDataComposite> findCallTimeDataForAutoGroup(Subject subject, int parentResourceId, int childResourceTypeId, long beginTime, long endTime, PageControl pageControl) { return findCallTimeDataForContext(subject, EntityContext.forAutoGroup(parentResourceId, childResourceTypeId), beginTime, endTime, null, pageControl); } @Override public PageList<CallTimeDataComposite> findCallTimeDataForContext(Subject subject, EntityContext context, long beginTime, long endTime, String destination, PageControl pageControl) { CallTimeDataCriteria criteria = new CallTimeDataCriteria(); criteria.addFilterBeginTime(beginTime); criteria.addFilterEndTime(endTime); if (destination != null && !destination.trim().equals("")) { criteria.addFilterDestination(destination); } criteria.setPageControl(pageControl); return findCallTimeDataForContext(subject, context, criteria); } @Override public PageList<CallTimeDataComposite> findCallTimeDataForContext(Subject subject, EntityContext context, CallTimeDataCriteria criteria) { PageControl pageControl = criteria.getPageControlOverrides(); if (pageControl != null) { pageControl.initDefaultOrderingField("SUM(calltimedatavalue.total)/SUM(calltimedatavalue.count)", PageOrdering.DESC); // only set if no ordering yet specified pageControl.addDefaultOrderingField("calltimedatavalue.key.callDestination", PageOrdering.ASC); // add this to sort, if not already specified } if (context.type == EntityContext.Type.Resource) { criteria.addFilterResourceId(context.resourceId); } else if (context.type == EntityContext.Type.ResourceGroup) { criteria.addFilterResourceGroupId(context.groupId); } else if (context.type == EntityContext.Type.AutoGroup) { criteria.addFilterAutoGroupParentResourceId(context.parentResourceId); criteria.addFilterAutoGroupResourceTypeId(context.resourceTypeId); } criteria.setSupportsAddSortId(false); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(subject, criteria); String replacementSelectList = "" // + " new org.rhq.core.domain.measurement.calltime.CallTimeDataComposite( " // + " calltimedatavalue.key.callDestination, " // + " MIN(calltimedatavalue.minimum), " // + " MAX(calltimedatavalue.maximum), " // + " SUM(calltimedatavalue.total), " // + " SUM(calltimedatavalue.count), " // + " SUM(calltimedatavalue.total) / SUM(calltimedatavalue.count) ) "; generator.alterProjection(replacementSelectList); generator.setGroupByClause("calltimedatavalue.key.callDestination"); if (authorizationManager.isInventoryManager(subject) == false) { generator.setAuthorizationResourceFragment(CriteriaQueryGenerator.AuthorizationTokenType.RESOURCE, "key.schedule.resource", subject.getId()); } //log.info(generator.getParameterReplacedQuery(false)); //log.info(generator.getParameterReplacedQuery(true)); CriteriaQueryRunner<CallTimeDataComposite> queryRunner = new CriteriaQueryRunner<CallTimeDataComposite>( criteria, generator, entityManager); PageList<CallTimeDataComposite> results = queryRunner.execute(); return results; } /* * internal method, do not expose to the remote API */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void insertCallTimeDataKeys(Set<CallTimeData> callTimeDataSet) { int[] results; String insertKeySql; PreparedStatement ps = null; Connection conn = null; try { conn = rhqDs.getConnection(); DatabaseType dbType = DatabaseTypeFactory.getDefaultDatabaseType(); if (dbType instanceof Postgresql83DatabaseType) { Statement st = null; try { // Take advantage of async commit here st = conn.createStatement(); st.execute("SET synchronous_commit = off"); } finally { JDBCUtil.safeClose(st); } } if (dbType instanceof PostgresqlDatabaseType || dbType instanceof OracleDatabaseType || dbType instanceof H2DatabaseType) { String keyNextvalSql = JDBCUtil.getNextValSql(conn, "RHQ_calltime_data_key"); insertKeySql = String.format(CALLTIME_KEY_INSERT_STATEMENT, keyNextvalSql); } else if (dbType instanceof SQLServerDatabaseType) { insertKeySql = CALLTIME_KEY_INSERT_STATEMENT_AUTOINC; } else { throw new IllegalArgumentException("Unknown database type, can't continue: " + dbType); } ps = conn.prepareStatement(insertKeySql); for (CallTimeData callTimeData : callTimeDataSet) { ps.setInt(1, callTimeData.getScheduleId()); ps.setInt(3, callTimeData.getScheduleId()); Set<String> callDestinations = callTimeData.getValues().keySet(); for (String callDestination : callDestinations) { // make sure the destination string is safe for storage, clip as needed String safeCallDestination = dbType.getString(callDestination, CallTimeDataKey.DESTINATION_MAX_LENGTH); ps.setString(2, safeCallDestination); ps.setString(4, safeCallDestination); ps.addBatch(); } } results = ps.executeBatch(); int insertedRowCount = 0; for (int i = 0; i < results.length; i++) { if (((results[i] < 0) || (results[i] > 1)) && (results[i] != -2)) // oracle returns -2 because it can't count updated rows { throw new MeasurementStorageException("Failed to insert call-time data key rows - result [" + results[i] + "] for batch command [" + i + "] is less than 0 or greater than 1."); } insertedRowCount += results[i] == -2 ? 1 : results[i]; // If Oracle returns -2, just count 1 row } log.debug("Inserted new call-time data key rows for " + ((insertedRowCount >= 0) ? insertedRowCount : "?") + " out of " + results.length + " reported key-value pairs."); } catch (SQLException e) { logSQLException("Failed to persist call-time data keys", e); } catch (Throwable t) { log.error("Failed to persist call-time data keys", t); } finally { JDBCUtil.safeClose(conn, ps, null); } } /* * internal method, do not expose to the remote API */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void insertCallTimeDataValues(Set<CallTimeData> callTimeDataSet) { int[] results; String insertValueSql; PreparedStatement ps = null; Connection conn = null; try { conn = rhqDs.getConnection(); DatabaseType dbType = DatabaseTypeFactory.getDatabaseType(conn); if (dbType instanceof Postgresql83DatabaseType) { Statement st = null; try { // Take advantage of async commit here st = conn.createStatement(); st.execute("SET synchronous_commit = off"); } finally { JDBCUtil.safeClose(st); } } if (dbType instanceof PostgresqlDatabaseType || dbType instanceof OracleDatabaseType || dbType instanceof H2DatabaseType) { String valueNextvalSql = JDBCUtil.getNextValSql(conn, "RHQ_calltime_data_value"); insertValueSql = String.format(CALLTIME_VALUE_INSERT_STATEMENT, valueNextvalSql); } else if (dbType instanceof SQLServerDatabaseType) { insertValueSql = CALLTIME_VALUE_INSERT_STATEMENT_AUTOINC; } else { throw new IllegalArgumentException("Unknown database type, can't continue: " + dbType); } ps = conn.prepareStatement(insertValueSql); for (CallTimeData callTimeData : callTimeDataSet) { ps.setInt(7, callTimeData.getScheduleId()); Set<String> callDestinations = callTimeData.getValues().keySet(); for (String callDestination : callDestinations) { CallTimeDataValue callTimeDataValue = callTimeData.getValues().get(callDestination); ps.setLong(1, callTimeDataValue.getBeginTime()); ps.setLong(2, callTimeDataValue.getEndTime()); ps.setDouble(3, callTimeDataValue.getMinimum()); ps.setDouble(4, callTimeDataValue.getMaximum()); ps.setDouble(5, callTimeDataValue.getTotal()); ps.setLong(6, callTimeDataValue.getCount()); // make sure the destination string is safe for storage, clip as needed String safeCallDestination = dbType.getString(callDestination, CallTimeDataKey.DESTINATION_MAX_LENGTH); ps.setString(8, safeCallDestination); ps.addBatch(); } } results = ps.executeBatch(); int insertedRowCount = 0; for (int i = 0; i < results.length; i++) { if ((results[i] != 1) && (results[i] != -2)) // Oracle likes to return -2 becuase it doesn't track batch update counts { throw new MeasurementStorageException("Failed to insert call-time data value rows - result [" + results[i] + "] for batch command [" + i + "] does not equal 1."); } insertedRowCount += results[i] == -2 ? 1 : results[i]; // If Oracle returns -2, just count 1 row; } notifyAlertConditionCacheManager("insertCallTimeDataValues", callTimeDataSet.toArray(new CallTimeData[callTimeDataSet.size()])); if (insertedRowCount > 0) { MeasurementMonitor.getMBean().incrementCalltimeValuesInserted(insertedRowCount); log.debug("Inserted " + insertedRowCount + " call-time data value rows."); } } catch (SQLException e) { logSQLException("Failed to persist call-time data values", e); } catch (Throwable t) { log.error("Failed to persist call-time data values", t); } finally { JDBCUtil.safeClose(conn, ps, null); } } private void notifyAlertConditionCacheManager(String callingMethod, CallTimeData... data) { AlertConditionCacheStats stats = alertConditionCacheManager.checkConditions(data); log.debug(callingMethod + ": " + stats.toString()); } private void logSQLException(String message, SQLException e) { SQLException mainException = e; StringBuilder causes = new StringBuilder(); int i = 1; while ((e = e.getNextException()) != null) { causes.append(i++).append("\n\t").append(e); } log.error(message + " - causes: " + causes, mainException); } }