com.nimbusds.infinispan.persistence.sql.ExpiredEntryReaper Maven / Gradle / Ivy
package com.nimbusds.infinispan.persistence.sql;
import com.codahale.metrics.Timer;
import com.nimbusds.infinispan.persistence.common.InfinispanEntry;
import net.jcip.annotations.ThreadSafe;
import org.infinispan.metadata.InternalMetadata;
import org.infinispan.metadata.impl.PrivateMetadata;
import org.infinispan.persistence.spi.AdvancedCacheExpirationWriter;
import org.infinispan.persistence.spi.AdvancedCacheWriter;
import org.infinispan.persistence.spi.MarshallableEntry;
import org.infinispan.persistence.spi.MarshallableEntryFactory;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Record;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jooq.impl.DSL.table;
/**
* Expired entry reaper.
*/
@ThreadSafe
class ExpiredEntryReaper {
/**
* The maximum number of entries in a delete batch.
*/
public static final int DELETE_BATCH_SIZE = 100;
/**
* The Infinispan marshallable entry factory.
*/
private final MarshallableEntryFactory mEntryFactory;
/**
* The DSL context.
*/
protected final DSLContext dsl;
/**
* The SQL record transformer.
*/
protected final SQLRecordTransformer recordTransformer;
/**
* The SQL record wrapper.
*/
protected final Function recordWrapper;
/**
* Times delete operations.
*/
private final Timer deleteTimer;
/**
* Creates a new reaper for expired entries.
*
* @param mEntryFactory The Infinispan marshallable entry factory.
* @param dsl The DSL context.
* @param recordTransformer The SQL record transformer.
* @param recordWrapper The SQL record wrapper to use.
* @param deleteTimer The SQL delete timer to use.
*/
public ExpiredEntryReaper(final MarshallableEntryFactory mEntryFactory,
final DSLContext dsl,
final SQLRecordTransformer recordTransformer,
final Function recordWrapper,
final Timer deleteTimer) {
assert mEntryFactory != null;
this.mEntryFactory = mEntryFactory;
assert dsl != null;
this.dsl = dsl;
assert recordTransformer != null;
this.recordTransformer = recordTransformer;
assert recordWrapper != null;
this.recordWrapper = recordWrapper;
assert deleteTimer != null;
this.deleteTimer = deleteTimer;
}
/**
* Returns the marhshallable entry for this Infinispan entry.
*
* @param en The Infinispan entry.
*
* @return The marshallable entry.
*/
protected MarshallableEntry toMarshallableEntry(final InfinispanEntry en) {
return mEntryFactory.create(
en.getKey(),
en.getValue(),
en.getMetadata(),
PrivateMetadata.empty(),
en.created(),
en.lastUsed()
);
}
/**
* Resolves the {@code WHERE} condition to retrieve only expired
* records.
*
* @return The expired condition.
*/
Collection resolveExpiredCondition() {
Condition expiredCondition = recordTransformer.getExpiredCondition(System.currentTimeMillis());
return expiredCondition != null ? Collections.singletonList(expiredCondition) : Collections.emptyList();
}
/**
* Purges the expired persisted entries.
*
* @param purgeListener The purge listener. Must not be {@code null}.
*/
public void purgeWithKeyListener(final AdvancedCacheWriter.PurgeListener super K> purgeListener) {
purge(en -> {
// Notify listener, interested in the Infinispan key
purgeListener.entryPurged(en.getKey());
});
}
/**
* Purges the expired persisted entries, with an extended listener for
* the complete purged entry.
*
* @param purgeListener The purge listener. Must not be {@code null}.
*/
public void purgeWithEntryListener(final AdvancedCacheExpirationWriter.ExpirationPurgeListener purgeListener) {
purge(en -> {
// Notify listener, interested in the Infinispan entry
purgeListener.marshalledEntryPurged(toMarshallableEntry(en));
});
}
/**
* Purges the expired persisted entries.
*
* @param purgeListener The purge listener. Must not be {@code null}.
*/
private void purge(final Consumer> purgeListener) {
// The entries for deletion
Queue> forDeletion = new LinkedList<>();
var numRetrieved = new AtomicLong();
var numDeleted = new AtomicLong();
dsl.select()
.from(table(recordTransformer.getTableName()))
.where(resolveExpiredCondition())
.stream()
.forEach(record -> {
RetrievedSQLRecord retrievedRecord = recordWrapper.apply(record);
numRetrieved.incrementAndGet();
InfinispanEntry infinispanEntry;
try {
infinispanEntry = recordTransformer.toInfinispanEntry(retrievedRecord);
} catch (Exception e) {
logIllegalRecordError(retrievedRecord);
return;
}
InternalMetadata metadata = infinispanEntry.getMetadata();
if (metadata != null && metadata.isExpired(System.currentTimeMillis())) {
// Add record for deletion
forDeletion.offer(infinispanEntry);
}
if (forDeletion.size() >= DELETE_BATCH_SIZE) {
// We have accumulated enough entries for deletion
numDeleted.addAndGet(delete(forDeletion, purgeListener));
}
});
// Delete any remaining
numDeleted.addAndGet(delete(forDeletion, purgeListener));
Loggers.SQL_LOG.debug("[IS0128] SQL store: Purged {} expired out of {} {} cache entries",
numDeleted, numRetrieved, recordTransformer.getTableName());
}
/**
* Deletes the specified entries.
*
* @param entries The entries to delete. Must not be
* {@code null}.
* @param purgeListener The purge listener. Must not be {@code null}.
*
* @return The number of actually deleted entries.
*/
protected long delete(final Queue> entries,
final Consumer> purgeListener) {
int numDeleted = 0;
while (true) {
InfinispanEntry en = entries.poll();
if (en == null) {
// Queue is empty
return numDeleted;
}
// Delete SQL record
int result;
try (Timer.Context timerCtx = deleteTimer.time()) {
result = dsl.deleteFrom(table(recordTransformer.getTableName()))
.where(recordTransformer.resolveSelectionConditions(en.getKey()))
.execute();
} catch (Exception e) {
Loggers.SQL_LOG.error("[IS0142] SQL store: Purge delete error: {}", e.getMessage());
continue;
}
if (result == 1) {
purgeListener.accept(en);
numDeleted++;
}
}
}
protected void logIllegalRecordError(final RetrievedSQLRecord sqlRecord) {
Loggers.SQL_LOG.error("[IS0141] SQL store: Illegal SQL record in {} cache entries (BASE64 encoded for safety): {}",
recordTransformer.getTableName(),
Base64.getEncoder().encodeToString(sqlRecord.toString().getBytes(StandardCharsets.UTF_8))
);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy