All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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