package br.com.citframework.integracao.core;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.log4j.Logger;
import br.com.citframework.integracao.ConnectionProvider;
/**
* {@code THREAD-SAFE} - {@link SequenceBlockCache} gerencia um n�mero arbitr�rio de seque�ncias utilizadas em rela��es em DBs, utilizando o padr�o SEQUENCE BLOCK.
*
* @author bruno.ribeiro - <a href="mailto:bruno.ribeiro@centrait.com.br">bruno.ribeiro@centrait.com.br</a>
* @date 18/08/2014
*/
public final class SequenceBlockCache {
private String alias;
private Connection connection;
private static final Logger LOGGER = Logger.getLogger(SequenceBlockCache.class);
private static final int DEFAULT_SEQUENCE_BLOCK_INCREMENT = 50;
private static final String LOGGER_BLOCK_INCREMENT_CONST = ", blockIncrement=";
private static final String LOGGER_CALLED_CONST = ") called.";
private static final String LOGGER_COULD_NOT_CREATE_NEW_SEQUENCE_RECORD = ": Could not create new sequence record ('";
private static final String LOGGER_GET_NEXTID_CONST = ".getNextId('";
private static final String LOGGER_THREAD_CONST = "Thread @";
private static final String SQL_SELECT_CURRENT_ID = "SELECT MAX(%s) FROM %s";
private static final String SQL_SELECT_CONTROLLER = "SELECT last_id FROM sequence_block_controller WHERE UPPER(sequence_name) = UPPER(?)";
private static final String SQL_INSERT_CONTROLLER = "INSERT INTO sequence_block_controller (sequence_name, last_id) VALUES (?, ?)";
private static final String SQL_UPDATE_CONTROLLER = "UPDATE sequence_block_controller SET last_id = ? WHERE UPPER(sequence_name) = UPPER(?) AND last_id = ? AND id > 0";
private final Map<String, SequenceBlock> cachedSequenceBlocksMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> sequenceLockMap = new ConcurrentHashMap<>();
SequenceBlockCache() {}
/**
* Constr�i um {@link SequenceBlockCache} inicializando {@link SequenceBlockCache#connection}
*/
public SequenceBlockCache(final String alias) {
this.alias = alias;
}
/**
* Recupera objeto para sincronizar apenas a {@link SequenceBlock} que est� sendo executada pela thread, para n�o bloquear todo o cache<br>
*
* @param sequenceName
* nome que reprensenta o {@link SequenceBlock}
* @return objeto para sincroniza��o
*/
private Object getSequenceLockObject(final String sequenceName) {
Object lockObject = sequenceLockMap.get(sequenceName);
if (lockObject == null) {
lockObject = new Object();
final Object prevLockObject = sequenceLockMap.putIfAbsent(sequenceName, lockObject);
if (prevLockObject != null) {
lockObject = prevLockObject;
}
}
return lockObject;
}
/**
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @return Long
*/
private Long getCurrentSequenceNumberFromDb(final String tableName, final String fieldName, final Connection conn) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + ".getCurrentSequenceNumberFromDb('"
+ tableName + "'" + LOGGER_CALLED_CONST);
Long res = null;
final String sequenceName = tableName + "_" + fieldName;
try (PreparedStatement stmt = conn.prepareStatement(SQL_SELECT_CONTROLLER);) {
stmt.setString(1, sequenceName);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
res = rs.getLong(1);
}
}
} catch (final SQLException ex) {
LOGGER.error(ex.getMessage(), ex);
throw new RuntimeException(ex);
}
return res;
}
/**
* Constr�i um novo {@link SequenceBlock} para {@code tableName} e {@code fieldName}
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @param lastUsedValue
* �ltimo valor usado do {@link SequenceBlock}
* @param blockIncrement
* incremente a ser considerado no bloco
* @return SequenceBlock
*/
private SequenceBlock reserveNextSequenceBlock(final String tableName, final String fieldName, final long lastUsedValue, final int blockIncrement, final Connection conn) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + ".reserveNextSequenceBlock('"
+ tableName + "', lastUsedValue=" + lastUsedValue + LOGGER_BLOCK_INCREMENT_CONST + blockIncrement + LOGGER_CALLED_CONST);
long updCount = 0;
SequenceBlock res = null;
final String sequenceName = tableName + "_" + fieldName;
final int localBlockIncrement = blockIncrement < 1 ? DEFAULT_SEQUENCE_BLOCK_INCREMENT : blockIncrement;
final long startOfBlock = lastUsedValue + 1;
final long endOfBlock = startOfBlock + localBlockIncrement;
try {
try (PreparedStatement stmt = conn.prepareStatement(SQL_UPDATE_CONTROLLER)) {
stmt.setLong(1, endOfBlock - 1);
stmt.setString(2, sequenceName);
stmt.setLong(3, lastUsedValue);
updCount = stmt.executeUpdate();
}
} catch (final SQLException ex) {
throw new RuntimeException(ex);
}
if (updCount == 1) {
res = new SequenceBlock(startOfBlock, endOfBlock);
}
return res;
}
/**
* Retorna o {@link SequenceBlock} correspondente a tabela {@code tableName} e {@code fieldName}. Se necess�rio, recupera no DB
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @param blockIncrement
* incremente a ser considerado no bloco
* @return SequenceBlock
*/
private SequenceBlock reserveNextSequenceBlock(final String tableName, final String fieldName, final int blockIncrement) {
SequenceBlock sequenceBlock = null;
final String sequenceName = tableName + "_" + fieldName;
try (final Connection conn = this.getConnection()) {
while (sequenceBlock == null) {
Long currentValue = this.getCurrentSequenceNumberFromDb(tableName, fieldName, conn);
if (currentValue == null) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": Creating new sequence record for sequenceName='" + sequenceName
+ "'.");
currentValue = this.createAndGetCurrentSequenceValue(tableName, fieldName, conn);
if (currentValue == null) {
throw new RuntimeException(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": The sequence '" + sequenceName
+ "' could neither be created nor updated (unknown server error).");
}
}
final long lastUsedValue = currentValue;
if (lastUsedValue < 0) {
throw new IllegalStateException(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": Found negative value for sequence '"
+ sequenceName + "' - check database SEQUENCE_BLOCK_CONTROLLER!");
}
sequenceBlock = this.reserveNextSequenceBlock(tableName, fieldName, currentValue, blockIncrement, conn);
if (sequenceBlock == null) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode())
+ ": chosen as victim during concurrent block reservation - going to try again ...");
}
}
} catch (final SQLException ex) {
LOGGER.warn(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + LOGGER_COULD_NOT_CREATE_NEW_SEQUENCE_RECORD + sequenceName + "').", ex);
}
return sequenceBlock;
}
/**
* Creates the new sequence and returns the current value of the sequence afterwards.
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @return
*/
private Long createAndGetCurrentSequenceValue(final String tableName, final String fieldName, final Connection conn) {
final String sequenceName = tableName + "_" + fieldName;
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + ".createAndGetCurrentSequenceValue("
+ sequenceName + LOGGER_CALLED_CONST);
long maxValue = 0;
final String sql = String.format(SQL_SELECT_CURRENT_ID, fieldName, tableName);
try (PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
maxValue = rs.getLong(1);
}
} catch (final SQLException ex) {
LOGGER.warn(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + LOGGER_COULD_NOT_CREATE_NEW_SEQUENCE_RECORD + sequenceName + "').", ex);
}
try {
try (PreparedStatement stmt = conn.prepareStatement(SQL_INSERT_CONTROLLER)) {
stmt.setString(1, sequenceName);
stmt.setLong(2, maxValue);
stmt.executeUpdate();
}
} catch (final SQLException ex) {
LOGGER.warn(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + LOGGER_COULD_NOT_CREATE_NEW_SEQUENCE_RECORD + sequenceName + "').", ex);
}
return this.getCurrentSequenceNumberFromDb(tableName, fieldName, conn);
}
/**
* {@code THREAD-SAFE} - Retorna o {@link SequenceBlock} correspondente a tabela {@code tableName} e {@code fieldName}
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @param blockIncrement
* incremente a ser considerado no bloco
* @return SequenceBlock
*/
private SequenceBlock getSequenceBlock(final String tableName, final String fieldName, final int blockIncrement) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + ".getSequenceBlock('" + tableName
+ "'" + LOGGER_BLOCK_INCREMENT_CONST + blockIncrement + LOGGER_CALLED_CONST);
final String sequenceName = tableName + "_" + fieldName;
SequenceBlock sequenceBlock = cachedSequenceBlocksMap.get(sequenceName);
if (sequenceBlock == null || sequenceBlock.isExhausted()) {
// n�o sincroniza globalmente, mas por sequence block!
synchronized (this.getSequenceLockObject(sequenceName)) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": Performing lookup in sequence block cache");
sequenceBlock = cachedSequenceBlocksMap.get(sequenceName);
if (sequenceBlock == null || sequenceBlock.isExhausted()) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": New sequence block required!");
sequenceBlock = this.reserveNextSequenceBlock(tableName, fieldName, blockIncrement);
cachedSequenceBlocksMap.put(sequenceName, sequenceBlock);
} else {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": Found existing sequence block, no need for db-access.");
}
}
}
return sequenceBlock;
}
/**
* Retorna o pr�ximo id para o {@code fieldName} na {@code tableName}
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @param blockIncrement
* incremente a ser considerado no bloco
* @return long pr�ximo id
*/
public long getNextId(final String tableName, final String fieldName, int blockIncrement) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + LOGGER_GET_NEXTID_CONST + tableName
+ "'" + LOGGER_BLOCK_INCREMENT_CONST + blockIncrement + LOGGER_CALLED_CONST);
if (blockIncrement < 1) {
blockIncrement = DEFAULT_SEQUENCE_BLOCK_INCREMENT;
}
long res = -1;
while (res < 0) {
res = this.getSequenceBlock(tableName, fieldName, blockIncrement).getNextId();
}
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + LOGGER_GET_NEXTID_CONST + tableName
+ "'" + LOGGER_BLOCK_INCREMENT_CONST + blockIncrement + ") return " + res + " .");
return res;
}
/**
* Retorna o pr�ximo id para o {@code fieldName} na {@code tableName}
*
* @param tableName
* nome da tabela a ser recuperado o pr�ximo id
* @param fieldName
* nome do campo na tabela a ser recuperado o pr�ximo id
* @return long pr�ximo id
*/
public long getNextId(final String tableName, final String fieldName) {
LOGGER.debug(LOGGER_THREAD_CONST + Integer.toHexString(Thread.currentThread().hashCode()) + ": " + this.getClass().getSimpleName() + LOGGER_GET_NEXTID_CONST + tableName
+ "'" + LOGGER_CALLED_CONST);
return this.getNextId(tableName, fieldName, DEFAULT_SEQUENCE_BLOCK_INCREMENT);
}
/**
* Recupera a {@link Connection} usada para recuperar o maior id nas rela��es
*
* @return {@link Connection}
* @throws SQLException
*/
private Connection getConnection() {
try {
if (connection == null || connection.isClosed()) {
connection = ConnectionProvider.getConnection(alias);
}
} catch (final Exception e) {
LOGGER.warn(e.getMessage(), e);
}
return connection;
}
@Override
protected void finalize() throws Throwable {
if (connection != null && !connection.isClosed()) {
connection.close();
}
super.finalize();
}
}