package org.infinispan.persistence.jdbc.stringbased;
import static org.infinispan.persistence.PersistenceUtil.getExpiryTime;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import javax.transaction.Transaction;
import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.marshall.StreamingMarshaller;
import org.infinispan.commons.persistence.Store;
import org.infinispan.commons.util.Util;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.executors.ExecutorAllCompletionService;
import org.infinispan.filter.KeyFilter;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.marshall.core.MarshalledEntryFactory;
import org.infinispan.persistence.TaskContextImpl;
import org.infinispan.persistence.jdbc.JdbcUtil;
import org.infinispan.persistence.jdbc.configuration.JdbcStringBasedStoreConfiguration;
import org.infinispan.persistence.jdbc.connectionfactory.ConnectionFactory;
import org.infinispan.persistence.jdbc.connectionfactory.ManagedConnectionFactory;
import org.infinispan.persistence.jdbc.logging.Log;
import org.infinispan.persistence.jdbc.table.management.TableManager;
import org.infinispan.persistence.jdbc.table.management.TableManagerFactory;
import org.infinispan.persistence.keymappers.Key2StringMapper;
import org.infinispan.persistence.keymappers.TwoWayKey2StringMapper;
import org.infinispan.persistence.keymappers.UnsupportedKeyTypeException;
import org.infinispan.persistence.spi.AdvancedLoadWriteStore;
import org.infinispan.persistence.spi.InitializationContext;
import org.infinispan.persistence.spi.PersistenceException;
import org.infinispan.persistence.spi.TransactionalCacheWriter;
import org.infinispan.persistence.support.BatchModification;
import org.infinispan.util.KeyValuePair;
import org.infinispan.util.TimeService;
import org.infinispan.util.logging.LogFactory;
/**
* {@link org.infinispan.persistence.spi.AdvancedCacheLoader} implementation that stores the entries in a database. In contrast to the
* {@link org.infinispan.persistence.jdbc.binary.JdbcBinaryStore}, this cache store will store each entry within a row
* in the table (rather than grouping multiple entries into an row). This assures a finer grained granularity for all
* operation, and better performance. In order to be able to store non-string keys, it relies on an {@link
* org.infinispan.persistence.keymappers.Key2StringMapper}.
* <p/>
* Note that only the keys are stored as strings, the values are still saved as binary data. Using a character
* data type for the value column will result in unmarshalling errors.
* <p/>
* The actual storage table is defined through configuration {@link org.infinispan.persistence.jdbc.configuration.JdbcStringBasedStoreConfiguration}. The table can
* be
* created/dropped on-the-fly, at deployment time. For more details consult javadoc for {@link
* org.infinispan.persistence.jdbc.configuration.JdbcStringBasedStoreConfiguration}.
* <p/>
* It is recommended to use {@link JdbcStringBasedStore}} over
* {@link org.infinispan.persistence.jdbc.binary.JdbcBinaryStore}} whenever it is possible, as is has a better performance.
* One scenario in which this is not possible to use it though, is when you can't write an {@link org.infinispan.persistence.keymappers.Key2StringMapper}} to map the
* keys to to string objects (e.g. when you don't have control over the types of the keys, for whatever reason).
* <p/>
* <b>Preload</b>.In order to support preload functionality the store needs to read the string keys from the database and transform them
* into the corresponding key objects. {@link org.infinispan.persistence.keymappers.Key2StringMapper} only supports
* key to string transformation(one way); in order to be able to use preload one needs to specify an
* {@link org.infinispan.persistence.keymappers.TwoWayKey2StringMapper}, which extends {@link org.infinispan.persistence.keymappers.Key2StringMapper} and
* allows bidirectional transformation.
* <p/>
* <b>Rehashing</b>. When a node leaves/joins, Infinispan moves around persistent state as part of rehashing process.
* For this it needs access to the underlaying key objects, so if distribution is used, the mapper needs to be an
* {@link org.infinispan.persistence.keymappers.TwoWayKey2StringMapper} otherwise the cache won't start (same constraint as with preloading).
*
* @author Mircea.Markus@jboss.com
* @see org.infinispan.persistence.keymappers.Key2StringMapper
* @see org.infinispan.persistence.keymappers.DefaultTwoWayKey2StringMapper
*/
@Store(shared = true)
@ConfiguredBy(JdbcStringBasedStoreConfiguration.class)
public class JdbcStringBasedStore<K,V> implements AdvancedLoadWriteStore<K,V>, TransactionalCacheWriter<K,V> {
private static final Log log = LogFactory.getLog(JdbcStringBasedStore.class, Log.class);
private static final boolean trace = log.isTraceEnabled();
private final Map<Transaction, Connection> transactionConnectionMap = new ConcurrentHashMap<>();
private JdbcStringBasedStoreConfiguration configuration;
private GlobalConfiguration globalConfiguration;
private Key2StringMapper key2StringMapper;
private String cacheName;
private ConnectionFactory connectionFactory;
private MarshalledEntryFactory marshalledEntryFactory;
private StreamingMarshaller marshaller;
private TableManager tableManager;
private TimeService timeService;
private boolean isDistributedCache;
@Override
public void init(InitializationContext ctx) {
this.configuration = ctx.getConfiguration();
this.cacheName = ctx.getCache().getName();
this.globalConfiguration = ctx.getCache().getCacheManager().getCacheManagerConfiguration();
this.marshalledEntryFactory = ctx.getMarshalledEntryFactory();
this.marshaller = ctx.getMarshaller();
this.timeService = ctx.getTimeService();
this.isDistributedCache = ctx.getCache().getCacheConfiguration() != null && ctx.getCache().getCacheConfiguration().clustering().cacheMode().isDistributed();
}
@Override
public void start() {
if (configuration.manageConnectionFactory()) {
ConnectionFactory factory = ConnectionFactory.getConnectionFactory(configuration.connectionFactory().connectionFactoryClass());
factory.start(configuration.connectionFactory(), factory.getClass().getClassLoader());
initializeConnectionFactory(factory);
}
try {
Object mapper = Util.loadClassStrict(configuration.key2StringMapper(),
globalConfiguration.classLoader()).newInstance();
if (mapper instanceof Key2StringMapper) key2StringMapper = (Key2StringMapper) mapper;
} catch (Exception e) {
log.errorf("Trying to instantiate %s, however it failed due to %s", configuration.key2StringMapper(),
e.getClass().getName());
throw new IllegalStateException("This should not happen.", e);
}
if (trace) {
log.tracef("Using key2StringMapper: %s", key2StringMapper.getClass().getName());
}
if (configuration.preload()) {
enforceTwoWayMapper("preload");
}
if (isDistributedCache) {
enforceTwoWayMapper("distribution/rehashing");
}
}
@Override
public void stop() {
Throwable cause = null;
try {
tableManager.stop();
tableManager = null;
} catch (Throwable t) {
cause = t.getCause();
if (cause == null) cause = t;
log.debug("Exception while stopping", t);
}
try {
if (configuration.connectionFactory() instanceof ManagedConnectionFactory) {
log.tracef("Stopping mananged connection factory: %s", connectionFactory);
connectionFactory.stop();
}
} catch (Throwable t) {
if (cause == null) {
cause = t;
} else {
t.addSuppressed(cause);
}
log.debug("Exception while stopping", t);
}
if (cause != null) {
throw new PersistenceException("Exceptions occurred while stopping store", cause);
}
}
void initializeConnectionFactory(ConnectionFactory connectionFactory) throws PersistenceException {
this.connectionFactory = connectionFactory;
tableManager = getTableManager();
tableManager.setCacheName(cacheName);
tableManager.start();
}
public ConnectionFactory getConnectionFactory() {
return connectionFactory;
}
@Override
public void write(MarshalledEntry entry) {
Connection connection = null;
String keyStr = key2Str(entry.getKey());
try {
connection = connectionFactory.getConnection();
write(entry, connection, keyStr);
} catch (SQLException ex) {
log.sqlFailureStoringKey(keyStr, ex);
throw new PersistenceException(String.format("Error while storing string key to database; key: '%s'", keyStr), ex);
} catch (InterruptedException e) {
if (trace) {
log.trace("Interrupted while marshalling to store");
}
Thread.currentThread().interrupt();
} finally {
connectionFactory.releaseConnection(connection);
}
}
private void write(MarshalledEntry entry, Connection connection) throws SQLException, InterruptedException {
write(entry, connection, key2Str(entry.getKey()));
}
private void write(MarshalledEntry entry, Connection connection, String keyStr) throws SQLException, InterruptedException {
if (tableManager.isUpsertSupported()) {
executeUpsert(connection, entry, keyStr);
} else {
executeLegacyUpdate(connection, entry, keyStr);
}
}
private void executeUpsert(Connection connection, MarshalledEntry entry, String keyStr)
throws InterruptedException, SQLException {
PreparedStatement ps = null;
String sql = tableManager.getUpsertRowSql();
if (trace) {
log.tracef("Running sql '%s'. Key string is '%s'", sql, keyStr);
} try {
ps = connection.prepareStatement(sql);
prepareUpdateStatement(entry, keyStr, ps);
ps.executeUpdate();
} finally {
JdbcUtil.safeClose(ps);
}
}
private void executeLegacyUpdate(Connection connection, MarshalledEntry entry, String keyStr)
throws InterruptedException, SQLException {
String sql = tableManager.getSelectIdRowSql();
if (trace) {
log.tracef("Running sql '%s'. Key string is '%s'", sql, keyStr);
}
PreparedStatement ps = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, keyStr);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
sql = tableManager.getUpdateRowSql();
} else {
sql = tableManager.getInsertRowSql();
}
JdbcUtil.safeClose(rs);
JdbcUtil.safeClose(ps);
if (trace) {
log.tracef("Running sql '%s'. Key string is '%s'", sql, keyStr);
}
ps = connection.prepareStatement(sql);
prepareUpdateStatement(entry, keyStr, ps);
ps.executeUpdate();
} finally {
JdbcUtil.safeClose(ps);
}
}
@Override
public MarshalledEntry load(Object key) {
String lockingKey = key2Str(key);
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
MarshalledEntry storedValue = null;
try {
String sql = tableManager.getSelectRowSql();
conn = connectionFactory.getConnection();
ps = conn.prepareStatement(sql);
ps.setString(1, lockingKey);
rs = ps.executeQuery();
if (rs.next()) {
InputStream inputStream = rs.getBinaryStream(2);
KeyValuePair<ByteBuffer, ByteBuffer> icv = unmarshall(inputStream);
storedValue = marshalledEntryFactory.newMarshalledEntry(key, icv.getKey(), icv.getValue());
}
} catch (SQLException e) {
log.sqlFailureReadingKey(key, lockingKey, e);
throw new PersistenceException(String.format(
"SQL error while fetching stored entry with key: %s, lockingKey: %s",
key, lockingKey), e);
} finally {
JdbcUtil.safeClose(rs);
JdbcUtil.safeClose(ps);
connectionFactory.releaseConnection(conn);
}
if (storedValue != null && storedValue.getMetadata() != null &&
storedValue.getMetadata().isExpired(timeService.wallClockTime())) {
return null;
}
return storedValue;
}
@Override
public void clear() {
Connection conn = null;
Statement statement = null;
try {
String sql = tableManager.getDeleteAllRowsSql();
conn = connectionFactory.getConnection();
statement = conn.createStatement();
int result = statement.executeUpdate(sql);
if (log.isTraceEnabled()) {
log.tracef("Successfully removed %d rows.", result);
}
} catch (SQLException ex) {
log.failedClearingJdbcCacheStore(ex);
throw new PersistenceException("Failed clearing cache store", ex);
} finally {
JdbcUtil.safeClose(statement);
connectionFactory.releaseConnection(conn);
}
}
@Override
public boolean delete(Object key) {
Connection connection = null;
PreparedStatement ps = null;
String keyStr = key2Str(key);
try {
String sql = tableManager.getDeleteRowSql();
if (trace) {
log.tracef("Running sql '%s' on %s", sql, keyStr);
}
connection = connectionFactory.getConnection();
ps = connection.prepareStatement(sql);
ps.setString(1, keyStr);
return ps.executeUpdate() == 1;
} catch (SQLException ex) {
log.sqlFailureRemovingKeys(ex);
throw new PersistenceException("Error while removing string keys from database", ex);
} finally {
JdbcUtil.safeClose(ps);
connectionFactory.releaseConnection(connection);
}
}
@Override
public void purge(Executor executor, PurgeListener purgeListener) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = tableManager.getSelectOnlyExpiredRowsSql();
conn = connectionFactory.getConnection();
ps = conn.prepareStatement(sql);
ps.setLong(1, timeService.wallClockTime());
rs = ps.executeQuery();
try (PreparedStatement batchDelete = conn.prepareStatement(tableManager.getDeleteRowSql())) {
int affectedRows = 0;
boolean twoWayMapperExists = key2StringMapper instanceof TwoWayKey2StringMapper;
while (rs.next()) {
affectedRows++;
String keyStr = rs.getString(2);
batchDelete.setString(1, keyStr);
batchDelete.addBatch();
if (twoWayMapperExists && purgeListener != null) {
Object key = ((TwoWayKey2StringMapper) key2StringMapper).getKeyMapping(keyStr);
purgeListener.entryPurged(key);
}
}
if (!twoWayMapperExists)
log.twoWayKey2StringMapperIsMissing(TwoWayKey2StringMapper.class.getSimpleName());
if (affectedRows > 0) {
int[] result = batchDelete.executeBatch();
if (trace) {
log.tracef("Successfully purged %d rows.", result.length);
}
}
}
} catch (SQLException ex) {
log.failedClearingJdbcCacheStore(ex);
throw new PersistenceException("Failed clearing string based JDBC store", ex);
} finally {
JdbcUtil.safeClose(rs);
JdbcUtil.safeClose(ps);
connectionFactory.releaseConnection(conn);
}
}
@Override
public boolean contains(Object key) {
//we can do better if needed...
return load(key) != null;
}
@Override
public void process(final KeyFilter filter, final CacheLoaderTask task, Executor executor, final boolean fetchValue, final boolean fetchMetadata) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
String sql = tableManager.getLoadNonExpiredAllRowsSql();
if (trace) {
log.tracef("Running sql %s", sql);
}
conn = connectionFactory.getConnection();
ps = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
ps.setLong(1, timeService.wallClockTime());
ps.setFetchSize(tableManager.getFetchSize());
rs = ps.executeQuery();
TaskContext taskContext = new TaskContextImpl();
ExecutorAllCompletionService ecs = new ExecutorAllCompletionService(executor);
while (rs.next()) {
String keyStr = rs.getString(2);
Object key = ((TwoWayKey2StringMapper) key2StringMapper).getKeyMapping(keyStr);
if (taskContext.isStopped()) break;
if (filter != null && !filter.accept(key))
continue;
InputStream inputStream = rs.getBinaryStream(1);
ecs.submit(() -> {
if (!taskContext.isStopped()) {
MarshalledEntry entry;
if (fetchValue || fetchMetadata) {
KeyValuePair<ByteBuffer, ByteBuffer> kvp = unmarshall(inputStream);
entry = marshalledEntryFactory.newMarshalledEntry(
key, fetchValue ? kvp.getKey() : null, fetchMetadata ? kvp.getValue() : null);
} else {
entry = marshalledEntryFactory.newMarshalledEntry(key, (Object) null, null);
}
task.processEntry(entry, taskContext);
}
return null;
});
}
ecs.waitUntilAllCompleted();
if (ecs.isExceptionThrown()) {
throw new PersistenceException("Execution exception!", ecs.getFirstException());
}
} catch (SQLException e) {
log.sqlFailureFetchingAllStoredEntries(e);
throw new PersistenceException("SQL error while fetching all StoredEntries", e);
} finally {
JdbcUtil.safeClose(rs);
JdbcUtil.safeClose(ps);
connectionFactory.releaseConnection(conn);
}
}
@Override
public void prepareWithModifications(Transaction transaction, BatchModification batchModification) throws PersistenceException {
try {
Connection connection = getTxConnection(transaction);
connection.setAutoCommit(false);
boolean upsertSupported = tableManager.isUpsertSupported();
try (PreparedStatement upsertBatch = upsertSupported ? connection.prepareStatement(tableManager.getUpsertRowSql()) : null;
PreparedStatement deleteBatch = connection.prepareStatement(tableManager.getDeleteRowSql())) {
for (MarshalledEntry entry : batchModification.getMarshalledEntries()) {
if (upsertSupported) {
String keyStr = key2Str(entry.getKey());
prepareUpdateStatement(entry, keyStr, upsertBatch);
upsertBatch.addBatch();
} else {
write(entry, connection);
}
}
for (Object key : batchModification.getKeysToRemove()) {
String keyStr = key2Str(key);
deleteBatch.setString(1, keyStr);
deleteBatch.addBatch();
}
if (upsertSupported && !batchModification.getMarshalledEntries().isEmpty())
upsertBatch.executeBatch();
if (!batchModification.getKeysToRemove().isEmpty())
deleteBatch.executeUpdate();
}
// We do not call connection.close() in the event of an exception, as close() on active Tx behaviour is implementation
// dependent. See https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#close--
} catch (SQLException | InterruptedException e) {
throw log.prepareTxFailure(e);
}
}
@Override
public void commit(Transaction tx) {
Connection connection;
try {
connection = getTxConnection(tx);
connection.commit();
} catch (SQLException e) {
log.sqlFailureTxCommit(e);
throw new PersistenceException(String.format("Error during commit of JDBC transaction (%s)", tx), e);
} finally {
destroyTxConnection(tx);
}
}
@Override
public void rollback(Transaction tx) {
Connection connection;
try {
connection = getTxConnection(tx);
connection.rollback();
} catch (SQLException e) {
log.sqlFailureTxRollback(e);
throw new PersistenceException(String.format("Error during rollback of JDBC transaction (%s)", tx), e);
} finally {
destroyTxConnection(tx);
}
}
private Connection getTxConnection(Transaction tx) {
Connection connection = transactionConnectionMap.get(tx);
if (connection == null) {
connection = connectionFactory.getConnection();
transactionConnectionMap.put(tx, connection);
}
return connection;
}
private void destroyTxConnection(Transaction tx) {
Connection connection = transactionConnectionMap.remove(tx);
if (connection != null)
connectionFactory.releaseConnection(connection);
}
@Override
public int size() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = connectionFactory.getConnection();
String sql = tableManager.getCountRowsSql();
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
rs.next();
return rs.getInt(1);
} catch (SQLException e) {
log.sqlFailureIntegratingState(e);
throw new PersistenceException("SQL failure while integrating state into store", e);
} finally {
JdbcUtil.safeClose(rs);
JdbcUtil.safeClose(ps);
connectionFactory.releaseConnection(conn);
}
}
private void prepareUpdateStatement(MarshalledEntry entry, String key, PreparedStatement ps) throws InterruptedException, SQLException {
ByteBuffer byteBuffer = marshall(new KeyValuePair(entry.getValueBytes(), entry.getMetadataBytes()));
ps.setBinaryStream(1, new ByteArrayInputStream(byteBuffer.getBuf(), byteBuffer.getOffset(), byteBuffer.getLength()), byteBuffer.getLength());
ps.setLong(2, getExpiryTime(entry.getMetadata()));
ps.setString(3, key);
}
private String key2Str(Object key) throws PersistenceException {
if (!key2StringMapper.isSupportedType(key.getClass())) {
throw new UnsupportedKeyTypeException(key);
}
String keyStr = key2StringMapper.getStringMapping(key);
return tableManager.isStringEncodingRequired() ? tableManager.encodeString(keyStr) : keyStr;
}
public TableManager getTableManager() {
if (tableManager == null)
tableManager = TableManagerFactory.getManager(connectionFactory, configuration);
return tableManager;
}
private void enforceTwoWayMapper(String where) throws PersistenceException {
if (!(key2StringMapper instanceof TwoWayKey2StringMapper)) {
log.invalidKey2StringMapper(where, key2StringMapper.getClass().getName());
throw new PersistenceException(String.format("Invalid key to string mapper : %s", key2StringMapper.getClass().getName()));
}
}
private ByteBuffer marshall(Object obj) throws PersistenceException, InterruptedException {
try {
return marshaller.objectToBuffer(obj);
} catch (IOException e) {
log.errorMarshallingObject(e, obj);
throw new PersistenceException("I/O failure while marshalling object: " + obj, e);
}
}
@SuppressWarnings("unchecked")
private <T> T unmarshall(InputStream inputStream) throws PersistenceException {
try {
return (T) marshaller.objectFromInputStream(inputStream);
} catch (IOException e) {
log.ioErrorUnmarshalling(e);
throw new PersistenceException("I/O error while unmarshalling from stream", e);
} catch (ClassNotFoundException e) {
log.unexpectedClassNotFoundException(e);
throw new PersistenceException("*UNEXPECTED* ClassNotFoundException. This should not happen as Bucket class exists", e);
}
}
}