/***************************************************************************** * * Copyright (C) Zenoss, Inc. 2010, all rights reserved. * * This content is made available according to terms specified in * License.zenoss under the directory where your Zenoss product is installed. * ****************************************************************************/ package org.zenoss.zep.dao.impl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.core.simple.SimpleJdbcInsertOperations; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.zenoss.zep.annotations.TransactionalReadOnly; import org.zenoss.zep.annotations.TransactionalRollbackAllExceptions; import org.zenoss.zep.dao.DaoCache; import org.zenoss.zep.dao.impl.compat.NestedTransactionCallback; import org.zenoss.zep.dao.impl.compat.NestedTransactionContext; import org.zenoss.zep.dao.impl.compat.NestedTransactionService; import javax.sql.DataSource; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class DaoCacheImpl implements DaoCache { private static final int DEFAULT_MAX_CACHE_ENTRIES = 500; private static final Logger logger = LoggerFactory.getLogger(DaoCacheImpl.class); private final DaoTableStringLruCache eventClassCache; private final DaoTableStringLruCache eventClassKeyCache; private final DaoTableStringLruCache monitorCache; private final DaoTableStringLruCache agentCache; private final DaoTableStringLruCache groupCache; private final DaoTableStringLruCache eventKeyCache; public DaoCacheImpl(DataSource ds, NestedTransactionService nestedTransactionService) { JdbcTemplate template = new JdbcTemplate(ds); this.eventClassCache = new DaoTableStringLruCache(template, nestedTransactionService, "event_class"); this.eventClassKeyCache = new DaoTableStringLruCache(template, nestedTransactionService, "event_class_key"); this.monitorCache = new DaoTableStringLruCache(template, nestedTransactionService, "monitor"); this.agentCache = new DaoTableStringLruCache(template, nestedTransactionService, "agent"); this.groupCache = new DaoTableStringLruCache(template, nestedTransactionService, "event_group"); this.eventKeyCache = new DaoTableStringLruCache(template, nestedTransactionService, "event_key"); } public void init() { this.eventClassCache.init(); this.eventClassKeyCache.init(); this.monitorCache.init(); this.agentCache.init(); this.groupCache.init(); this.eventKeyCache.init(); } private <T> int getIdFromName(final DaoTableCache<T> cache, final T name) { Integer cached = cache.getCache().getIdFromName(name); if (cached == null) { final int id = cache.insertOrSelect(name); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { cache.getCache().cache(name, id); } }); cached = id; } return cached; } private <T> T getNameFromId(final DaoTableCache<T> cache, final int id) { T cached = cache.getCache().getNameFromId(id); if (cached == null) { final T name = cache.findNameFromId(id); if (TransactionSynchronizationManager.isSynchronizationActive()) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { cache.getCache().cache(name, id); } }); } else { // Not part of a transaction - can safely cache cache.getCache().cache(name, id); } cached = name; } return cached; } @Override @TransactionalRollbackAllExceptions public int getEventClassId(String eventClass) { return getIdFromName(this.eventClassCache, eventClass); } @Override @TransactionalReadOnly public String getEventClassFromId(int id) { return getNameFromId(this.eventClassCache, id); } @Override @TransactionalRollbackAllExceptions public int getEventClassKeyId(String eventClassKey) { return getIdFromName(this.eventClassKeyCache, eventClassKey); } @Override @TransactionalReadOnly public String getEventClassKeyFromId(int id) { return getNameFromId(this.eventClassKeyCache, id); } @Override @TransactionalRollbackAllExceptions public int getMonitorId(String monitor) { return getIdFromName(this.monitorCache, monitor); } @Override @TransactionalReadOnly public String getMonitorFromId(int id) { return getNameFromId(this.monitorCache, id); } @Override @TransactionalRollbackAllExceptions public int getAgentId(String agent) { return getIdFromName(this.agentCache, agent); } @Override @TransactionalReadOnly public String getAgentFromId(int id) { return getNameFromId(this.agentCache, id); } @Override @TransactionalRollbackAllExceptions public int getEventGroupId(String eventGroup) { return getIdFromName(this.groupCache, eventGroup); } @Override @TransactionalReadOnly public String getEventGroupFromId(int id) { return getNameFromId(this.groupCache, id); } @Override @TransactionalRollbackAllExceptions public int getEventKeyId(String eventKey) { return getIdFromName(this.eventKeyCache, eventKey); } @Override @TransactionalReadOnly public String getEventKeyFromId(int id) { return getNameFromId(this.eventKeyCache, id); } private static class BiMap<T> { private final Map<T, Integer> nameToIdMap; private final Map<Integer, T> idToNameMap; private final int maxCache; /** * Creates a BiMap which is optionally bounded by the specified size. * * @param maxCache * The maximum size of the maps to keep. If this is greater * than zero, then this BiMap will function as a LRU cache. */ public BiMap(int maxCache) { this.nameToIdMap = new ConcurrentHashMap<T, Integer>(); if (maxCache <= 0) { this.idToNameMap = new ConcurrentHashMap<Integer, T>(); } else { this.idToNameMap = Collections.synchronizedMap(new LinkedHashMap<Integer, T>()); } this.maxCache = maxCache; } public Integer getIdFromName(T name) { return this.nameToIdMap.get(name); } public T getNameFromId(int id) { return this.idToNameMap.get(id); } public synchronized void cache(T name, int id) { this.idToNameMap.put(id, name); this.nameToIdMap.put(name, id); if (maxCache > 0 && idToNameMap.size() > maxCache) { final Iterator<Map.Entry<Integer, T>> it = this.idToNameMap .entrySet().iterator(); final Map.Entry<Integer, T> lru = it.next(); it.remove(); this.nameToIdMap.remove(lru.getValue()); } } } private static abstract class DaoTableCache<T> { protected final JdbcTemplate template; protected final NestedTransactionService nestedTransactionService; protected final String tableName; protected final BiMap<T> cache; protected final String selectNameByIdSql; protected final String selectIdByNameSql; protected static final String COLUMN_ID = "id"; protected static final String COLUMN_NAME = "name"; public DaoTableCache(JdbcTemplate template, NestedTransactionService nestedTransactionService, String tableName, BiMap<T> cache) { this.template = template; this.nestedTransactionService = nestedTransactionService; this.tableName = tableName; this.cache = cache; this.selectNameByIdSql = "SELECT " + COLUMN_NAME + " FROM " + this.tableName + " WHERE " + COLUMN_ID + "=?"; this.selectIdByNameSql = "SELECT " + COLUMN_ID + " FROM " + this.tableName + " WHERE " + COLUMN_NAME + "=?"; } public void init() { } public BiMap<T> getCache() { return cache; } public abstract int insertOrSelect(T name); public abstract T findNameFromId(int id); } private static class DaoTableStringLruCache extends DaoTableCache<String> { private SimpleJdbcInsertOperations jdbcInsert; public DaoTableStringLruCache(JdbcTemplate template, NestedTransactionService nestedTransactionService, String tableName) { this(template, nestedTransactionService, tableName, DEFAULT_MAX_CACHE_ENTRIES); } public DaoTableStringLruCache(JdbcTemplate template, NestedTransactionService nestedTransactionService, String tableName, final int limit) { super(template, nestedTransactionService, tableName, new BiMap<String>(limit)); } public void init() { super.init(); this.jdbcInsert = new SimpleJdbcInsert(template).withTableName(tableName) .usingColumns(COLUMN_NAME).usingGeneratedKeyColumns(COLUMN_ID) .withoutTableColumnMetaDataAccess(); } @Override public int insertOrSelect(final String name) { try { return this.template.queryForInt(selectIdByNameSql, name); }catch (EmptyResultDataAccessException ere) { try { return this.nestedTransactionService.executeInNestedTransaction(new NestedTransactionCallback<Integer>() { @Override public Integer doInNestedTransaction(NestedTransactionContext context) throws DataAccessException { final Map<String, Object> args = Collections.singletonMap(COLUMN_NAME, (Object) name); return jdbcInsert.executeAndReturnKey(args).intValue(); } }); }catch (DuplicateKeyException e) { return this.template.queryForInt(selectIdByNameSql, name); } } } @Override public String findNameFromId(int id) { try { return template.queryForObject(this.selectNameByIdSql, String.class, id); } catch (IncorrectResultSizeDataAccessException e) { logger.error("Database integrity error - id \"{}\" not found in table \"{}\". " + "Manual table recovery required.", id, this.tableName); throw e; } } } }