001package com.nimbusds.infinispan.persistence.sql;
002
003
004import java.time.Instant;
005import java.util.List;
006import java.util.Properties;
007import java.util.concurrent.Executor;
008import java.util.function.Predicate;
009import javax.sql.DataSource;
010
011import static org.jooq.impl.DSL.table;
012
013import com.codahale.metrics.MetricRegistry;
014import com.codahale.metrics.Timer;
015import com.codahale.metrics.health.HealthCheckRegistry;
016import com.zaxxer.hikari.HikariConfig;
017import com.zaxxer.hikari.HikariDataSource;
018import io.reactivex.Flowable;
019import net.jcip.annotations.ThreadSafe;
020import org.infinispan.commons.configuration.ConfiguredBy;
021import org.infinispan.commons.persistence.Store;
022import org.infinispan.marshall.core.MarshalledEntry;
023import org.infinispan.marshall.core.MarshalledEntryFactory;
024import org.infinispan.persistence.spi.InitializationContext;
025import org.infinispan.persistence.spi.PersistenceException;
026import org.jooq.DSLContext;
027import org.jooq.Merge;
028import org.jooq.Record;
029import org.jooq.SQLDialect;
030import org.jooq.conf.RenderNameStyle;
031import org.jooq.conf.Settings;
032import org.jooq.impl.DSL;
033import org.kohsuke.MetaInfServices;
034import org.reactivestreams.Publisher;
035
036import com.nimbusds.common.monitor.MonitorRegistries;
037import com.nimbusds.infinispan.persistence.common.InfinispanEntry;
038import com.nimbusds.infinispan.persistence.common.InfinispanStore;
039import com.nimbusds.infinispan.persistence.common.query.QueryExecutor;
040import com.nimbusds.infinispan.persistence.sql.config.SQLStoreConfiguration;
041import com.nimbusds.infinispan.persistence.sql.query.SQLQueryExecutor;
042import com.nimbusds.infinispan.persistence.sql.query.SQLQueryExecutorInitContext;
043
044
045/**
046 * SQL store for Infinispan 9.0+ caches and maps.
047 */
048@ThreadSafe
049@MetaInfServices
050@ConfiguredBy(SQLStoreConfiguration.class)
051@Store(shared = true)
052public class SQLStore<K,V> extends InfinispanStore<K,V> {
053
054
055        /**
056         * The SQL store configuration.
057         */
058        private SQLStoreConfiguration config;
059        
060        
061        /**
062         * Enables sharing of the Hikari SQL data sources.
063         */
064        private final static DataSources SHARED_DATA_SOURCES = new DataSources();
065        
066        
067        /**
068         * The Hikari SQL data source (with connection pool).
069         */
070        private HikariDataSource dataSource;
071        
072        
073        /**
074         * Wrap the SQL data source with jOOQ.
075         * See http://stackoverflow.com/a/31389342/429425
076         */
077        private DSLContext sql;
078
079
080        /**
081         * The SQL record transformer (to / from Infinispan entries).
082         */
083        private SQLRecordTransformer<K,V> sqlRecordTransformer;
084        
085        
086        /**
087         * The optional SQL query executor.
088         */
089        private SQLQueryExecutor<K,V> sqlQueryExecutor;
090
091
092        /**
093         * The marshalled Infinispan entry factory.
094         */
095        private MarshalledEntryFactory<K, V> marshalledEntryFactory;
096
097
098        /**
099         * Purges expired entries found in the SQL store, as indicated by
100         * their persisted metadata (optional, may be ignored / not stored).
101         */
102        private ExpiredEntryReaper<K,V> reaper;
103        
104        
105        /**
106         * SQL operation timers.
107         */
108        private SQLTimers timers;
109        
110        
111        /**
112         * jOOQ query fixes.
113         */
114        private JOOQFixes jooqFixes;
115
116
117        /**
118         * Loads an SQL record transformer with the specified class name.
119         *
120         * @param clazz The class. Must not be {@code null}.
121         *
122         * @return The SQL entry transformer.
123         */
124        @SuppressWarnings( "unchecked" )
125        private SQLRecordTransformer<K,V> loadRecordTransformerClass(final Class clazz) {
126
127                try {
128                        Class<SQLRecordTransformer<K,V>> genClazz = (Class<SQLRecordTransformer<K,V>>)clazz;
129                        return genClazz.getDeclaredConstructor().newInstance();
130                } catch (Exception e) {
131                        throw new PersistenceException("Couldn't load SQL record transformer class: " + e.getMessage(), e);
132                }
133        }
134        
135        
136        /**
137         * Loads an SQL query executor with the specified class name.
138         *
139         * @param clazz The class. Must not be {@code null}.
140         *
141         * @return The SQL query executor.
142         */
143        @SuppressWarnings( "unchecked" )
144        private SQLQueryExecutor<K,V> loadQueryExecutorClass(final Class clazz) {
145                
146                try {
147                        Class<SQLQueryExecutor<K,V>> genClazz = (Class<SQLQueryExecutor<K,V>>)clazz;
148                        return genClazz.getDeclaredConstructor().newInstance();
149                } catch (Exception e) {
150                        throw new PersistenceException("Couldn't load SQL query executor class: " + e.getMessage(), e);
151                }
152        }
153        
154        
155        /**
156         * Returns the SQL store configuration.
157         *
158         * @return The SQL store configuration, {@code null} if not
159         *         initialised.
160         */
161        public SQLStoreConfiguration getConfiguration() {
162                
163                return config;
164        }
165        
166        
167        /**
168         * Returns the underlying SQL data source.
169         *
170         * @return The underlying SQL data source, {@code null} if not
171         *         initialised.
172         */
173        public HikariDataSource getDataSource() {
174                
175                return dataSource;
176        }
177
178
179        @Override
180        @SuppressWarnings("unchecked")
181        public void init(final InitializationContext ctx) {
182
183                // This method will be invoked by the PersistenceManager during initialization. The InitializationContext
184                // contains:
185                // - this CacheLoader's configuration
186                // - the cache to which this loader is applied. Your loader might want to use the cache's name to construct
187                //   cache-specific identifiers
188                // - the StreamingMarshaller that needs to be used to marshall/unmarshall the entries
189                // - a TimeService which the loader can use to determine expired entries
190                // - a ByteBufferFactory which needs to be used to construct ByteBuffers
191                // - a MarshalledEntryFactory which needs to be used to construct entries from the data retrieved by the loader
192
193                super.init(ctx);
194                
195                this.config = ctx.getConfiguration();
196
197                Loggers.MAIN_LOG.info("[IS0100] SQL store: Infinispan cache store configuration for {}:", getCacheName());
198                config.log();
199                
200                Loggers.MAIN_LOG.info("[IS0140] SQL store: Expiration thread wake up interval for cache {}: {}", getCacheName(),
201                        ctx.getCache().getCacheConfiguration().expiration().wakeUpInterval());
202                
203                // Load and initialise the SQL record transformer
204                Loggers.MAIN_LOG.debug("[IS0101] Loading SQL record transformer class {} for cache {}...",
205                        config.getRecordTransformerClass(),
206                        getCacheName());
207                
208                sqlRecordTransformer = loadRecordTransformerClass(config.getRecordTransformerClass());
209                sqlRecordTransformer.init(() -> config.getSQLDialect());
210                
211                jooqFixes = new JOOQFixes(config.getSQLDialect(), sqlRecordTransformer.getCreateTableStatement());
212                
213                // Load and initialise the optional SQL query executor
214                if (config.getQueryExecutorClass() != null) {
215                        Loggers.MAIN_LOG.debug("[IS0201] Loading optional SQL query executor class {} for cache {}...",
216                                config.getQueryExecutorClass(),
217                                getCacheName());
218                        
219                        sqlQueryExecutor = loadQueryExecutorClass(config.getQueryExecutorClass());
220                        
221                        sqlQueryExecutor.init(new SQLQueryExecutorInitContext<K, V>() {
222                                @Override
223                                public DataSource getDataSource() {
224                                        return dataSource;
225                                }
226                                
227                                
228                                @Override
229                                public SQLRecordTransformer<K, V> getSQLRecordTransformer() {
230                                        return sqlRecordTransformer;
231                                }
232                                
233                                
234                                @Override
235                                public SQLDialect getSQLDialect() {
236                                        return config.getSQLDialect();
237                                }
238                        });
239                }
240
241                marshalledEntryFactory = (MarshalledEntryFactory<K, V>)ctx.getMarshalledEntryFactory();
242                
243                timers = new SQLTimers(ctx.getCache().getName() + ".");
244
245                Loggers.MAIN_LOG.info("[IS0102] Initialized SQL external store for cache {} with table {}",
246                        getCacheName(),
247                        sqlRecordTransformer.getTableName());
248        }
249        
250        
251        /**
252         * Returns the underlying SQL record transformer.
253         *
254         * @return The SQL record transformer, {@code null} if not initialised.
255         */
256        public SQLRecordTransformer<K, V> getSQLRecordTransformer() {
257                return sqlRecordTransformer;
258        }
259        
260        
261        @Override
262        public QueryExecutor<K, V> getQueryExecutor() {
263                
264                return sqlQueryExecutor;
265        }
266        
267        
268        /**
269         * Starts the Hikari data source using the existing configuration.
270         *
271         * @return The data source.
272         */
273        private HikariDataSource startDataSource() {
274                
275                Properties hikariProps = HikariConfigUtils.removeNonHikariProperties(config.properties());
276                HikariPoolName poolName = HikariPoolName.setDefaultPoolName(hikariProps, getCacheName());
277                
278                HikariConfig hikariConfig = new HikariConfig(hikariProps);
279                
280                MetricRegistry metricRegistry = MonitorRegistries.getMetricRegistry();
281                if (HikariConfigUtils.metricsAlreadyRegistered(poolName, metricRegistry)) {
282                        Loggers.MAIN_LOG.warn("[IS0130] SQL store: Couldn't register Dropwizard metrics: Existing registered metrics for " + getCacheName());
283                } else {
284                        hikariConfig.setMetricRegistry(metricRegistry);
285                }
286                
287                HealthCheckRegistry healthCheckRegistry = MonitorRegistries.getHealthCheckRegistry();
288                if (HikariConfigUtils.healthChecksAlreadyRegistered(poolName, healthCheckRegistry)) {
289                        Loggers.MAIN_LOG.warn("[IS0131] SQL store: Couldn't register Dropwizard health checks: Existing registered health checks for " + getCacheName());
290                } else {
291                        hikariConfig.setHealthCheckRegistry(healthCheckRegistry);
292                }
293                
294                return new HikariDataSource(hikariConfig);
295        }
296        
297        
298        @Override
299        public void start() {
300
301                // This method will be invoked by the PersistenceManager to start the CacheLoader. At this stage configuration
302                // is complete and the loader can perform operations such as opening a connection to the external storage,
303                // initialize internal data structures, etc.
304                
305                if (config.getConnectionPool() == null) {
306                        // Using own data source
307                        dataSource = startDataSource();
308                        SHARED_DATA_SOURCES.put(getCacheName(), dataSource);
309                } else {
310                        // Using shared data source
311                        dataSource = SHARED_DATA_SOURCES.get(config.getConnectionPool());
312                        if (dataSource == null) {
313                                SHARED_DATA_SOURCES.deferStart(getCacheName(), this);
314                                return;
315                        }
316                }
317                
318                // Init jOOQ SQL context
319                Settings jooqSettings = new Settings();
320                if (config.getSQLDialect().equals(SQLDialect.H2)) {
321                        // Quoted column names occasionally cause problems in H2
322                        jooqSettings.setRenderNameStyle(RenderNameStyle.AS_IS);
323                }
324                sql = DSL.using(dataSource, config.getSQLDialect(), jooqSettings);
325                
326                if (config.createTableIfMissing()) {
327                        try {
328                                int rows = sql.execute(sqlRecordTransformer.getCreateTableStatement());
329                                
330                                if (rows > 0) {
331                                        Loggers.MAIN_LOG.info("[IS0129] SQL store: Created table {} for cache {}", sqlRecordTransformer.getTableName(), getCacheName());
332                                }
333                                
334                        } catch (Exception e) {
335                                Loggers.MAIN_LOG.fatal("[IS0103] SQL store: Create table if not exists failed: {}: e", e.getMessage(), e);
336                                throw new PersistenceException(e.getMessage(), e);
337                        }
338                        
339                        // Alter table?
340                        if (sqlRecordTransformer instanceof SQLTableTransformer) {
341                                Loggers.MAIN_LOG.warn("[IS0133] SQL store: Found table transformer");
342                                List<String> transformQueries = ((SQLTableTransformer)sqlRecordTransformer)
343                                        .getTransformTableStatements(
344                                                SQLTableUtils.getColumnNames(table(sqlRecordTransformer.getTableName()), sql)
345                                        );
346                                if (transformQueries != null) {
347                                        for (String query: transformQueries) {
348                                                Loggers.MAIN_LOG.info("[IS0134] SQL store: About to execute table transform query: " + query);
349                                                sql.execute(query);
350                                        }
351                                }
352                        }
353                        
354                } else {
355                        Loggers.MAIN_LOG.info("[IS0132] SQL store: Skipped create table if missing step");
356                }
357
358                Loggers.MAIN_LOG.info("[IS0104] Started SQL external store connector for cache {} with table {}", getCacheName(), sqlRecordTransformer.getTableName());
359
360                reaper = new ExpiredEntryReaper<>(marshalledEntryFactory, sql, sqlRecordTransformer);
361        }
362
363
364        @Override
365        public void stop() {
366
367                super.stop();
368                
369                SHARED_DATA_SOURCES.remove(getCacheName());
370                
371                if (dataSource != null && config.getConnectionPool() == null) {
372                        dataSource.close();
373                }
374                
375                Loggers.MAIN_LOG.info("[IS0105] Stopped SQL store connector for cache {}",  getCacheName());
376        }
377
378
379        @SuppressWarnings("unchecked")
380        private K resolveKey(final Object key) {
381
382                if (key instanceof byte[]) {
383                        throw new PersistenceException("Cannot resolve " + getCacheName() + " cache key from byte[], enable compatibility mode");
384                }
385
386                return (K)key;
387        }
388
389
390        @Override
391        public boolean contains(final Object key) {
392
393                // This method will be invoked by the PersistenceManager to determine if the loader contains the specified key.
394                // The implementation should be as fast as possible, e.g. it should strive to transfer the least amount of data possible
395                // from the external storage to perform the check. Also, if possible, make sure the field is indexed on the external storage
396                // so that its existence can be determined as quickly as possible.
397                //
398                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
399                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
400
401                Loggers.SQL_LOG.trace("[IS0106] SQL store: Checking {} cache key {}", getCacheName(), key);
402                
403                Timer.Context timerCtx = timers.loadTimer.time();
404                
405                try {
406                        return sql.selectOne()
407                                .from(table(sqlRecordTransformer.getTableName()))
408                                .where(sqlRecordTransformer.resolveSelectionConditions(resolveKey(key)))
409                                .fetchOne() != null;
410                        
411                } catch (Exception e) {
412                        Loggers.SQL_LOG.error("[IS0107] {}: {}", e.getMessage(), e);
413                        throw new PersistenceException(e.getMessage(), e);
414                } finally {
415                        timerCtx.stop();
416                }
417        }
418
419
420        @Override
421        public MarshalledEntry<K,V> load(final Object key) {
422
423                // Fetches an entry from the storage using the specified key. The CacheLoader should retrieve from the external storage all
424                // of the data that is needed to reconstruct the entry in memory, i.e. the value and optionally the metadata. This method
425                // needs to return a MarshalledEntry which can be constructed as follows:
426                //
427                // ctx.getMarshalledEntryFactory().new MarshalledEntry(key, value, metadata);
428                //
429                // If the entry does not exist or has expired, this method should return null.
430                // If an error occurs while retrieving data from the external storage, this method should throw a PersistenceException
431                //
432                // Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
433                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
434                // If the loader needs to have knowledge of the key/value data beyond their binary representation, then it needs access to the key's and value's
435                // classes and the marshaller used to encode them.
436
437                Loggers.SQL_LOG.trace("[IS0108] SQL store: Loading {} cache entry with key {}", getCacheName(), key);
438                
439                final Record record;
440                
441                Timer.Context timerCtx = timers.loadTimer.time();
442                
443                try {
444                        record = sql.selectFrom(table(sqlRecordTransformer.getTableName()))
445                                .where(sqlRecordTransformer.resolveSelectionConditions(resolveKey(key)))
446                                .fetchOne();
447                        
448                } catch (Exception e) {
449                        Loggers.SQL_LOG.error("[IS0109] {}, {}", e.getMessage(), e);
450                        throw new PersistenceException(e.getMessage(), e);
451                } finally {
452                        timerCtx.stop();
453                }
454                
455                if (record == null) {
456                        // Not found
457                        Loggers.SQL_LOG.trace("[IS0110] SQL store: Record with key {} not found", key);
458                        return null;
459                }
460                
461                if (Loggers.SQL_LOG.isTraceEnabled()) {
462                        Loggers.SQL_LOG.trace("[IS0111] SQL store: Retrieved record: {}", record);
463                }
464
465                // Transform SQL entry to Infinispan entry
466                InfinispanEntry<K,V> infinispanEntry = sqlRecordTransformer.toInfinispanEntry(record);
467                
468                if (infinispanEntry.isExpired()) {
469                        Loggers.SQL_LOG.trace("[IS0135] SQL store: Record with key {} expired", key);
470                        return null;
471                }
472
473                return marshalledEntryFactory.newMarshalledEntry(
474                        infinispanEntry.getKey(),
475                        infinispanEntry.getValue(),
476                        infinispanEntry.getMetadata());
477        }
478
479
480        @Override
481        public boolean delete(final Object key) {
482
483                // The CacheWriter should remove from the external storage the entry identified by the specified key.
484                // Note that keys will be in the cache's native format, which means that if the cache is being used by a remoting protocol
485                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
486
487                Loggers.SQL_LOG.trace("[IS0112] SQL store: Deleting {} cache entry with key {}", getCacheName(), key);
488                
489                int deletedRows;
490                
491                Timer.Context timerCtx = timers.deleteTimer.time();
492                
493                try {
494                        deletedRows = sql.deleteFrom(table(sqlRecordTransformer.getTableName()))
495                                .where(sqlRecordTransformer.resolveSelectionConditions(resolveKey(key)))
496                                .execute();
497                        
498                } catch (Exception e) {
499                        Loggers.SQL_LOG.error("[IS0113] {}, {}", e.getMessage(), e);
500                        throw new PersistenceException(e.getMessage(), e);
501                } finally {
502                        timerCtx.stop();
503                }
504                
505                Loggers.SQL_LOG.trace("[IS0113] SQL store: Deleted {} record with key {}", deletedRows, key);
506                
507                if (deletedRows == 1) {
508                        return true;
509                } else if (deletedRows == 0) {
510                        return false;
511                } else {
512                        Loggers.SQL_LOG.error("[IS0114] Too many deleted rows ({}) for key {}", deletedRows, key);
513                        throw new PersistenceException("Too many deleted rows for key " + key);
514                }
515        }
516
517
518        @Override
519        public void write(final MarshalledEntry<? extends K, ? extends V> marshalledEntry) {
520
521                // The CacheWriter should write the specified entry to the external storage.
522                //
523                // The PersistenceManager uses MarshalledEntry as the default format so that CacheWriters can efficiently store data coming
524                // from a remote node, thus avoiding any additional transformation steps.
525                //
526                // Note that keys and values will be in the cache's native format, which means that if the cache is being used by a remoting protocol
527                // such as HotRod or REST and compatibility mode has not been enabled, then they will be encoded in a byte[].
528
529                Loggers.SQL_LOG.trace("[IS0115] SQL store: Writing {} cache entry {}", getCacheName(), marshalledEntry);
530                
531                Timer.Context timerCtx = timers.writeTimer.time();
532                
533                try {
534                        SQLRecord sqlRecord = sqlRecordTransformer.toSQLRecord(
535                                new InfinispanEntry<>(
536                                        marshalledEntry.getKey(),
537                                        marshalledEntry.getValue(),
538                                        marshalledEntry.getMetadata()));
539                        
540                        // Use H2 style MERGE, JOOQ will adapt it for the particular database
541                        // http://www.jooq.org/doc/3.8/manual/sql-building/sql-statements/merge-statement/
542                        Merge mergeStatement = sql.mergeInto(table(sqlRecordTransformer.getTableName()), sqlRecord.getFields().keySet())
543                                .key(sqlRecord.getKeyColumns())
544                                .values(sqlRecord.getFields().values());
545                        
546                        String sqlStatement = jooqFixes.fixMergeStatement(mergeStatement);
547                        
548                        int rows = sql.execute(sqlStatement);
549                                
550                        if (rows != 1) {
551                                
552                                if (SQLDialect.MYSQL.equals(config.getSQLDialect()) && rows == 2) {
553                                        // MySQL indicates UPDATE on INSERT by returning 2 num rows
554                                        return;
555                                }
556                                
557                                Loggers.SQL_LOG.error("[IS0116] SQL insert / update for key {} in table {} failed: Rows {}",
558                                        marshalledEntry.getKey(),sqlRecordTransformer.getTableName(),  rows);
559                                throw new PersistenceException("(Synthetic) SQL MERGE failed: Rows " + rows);
560                        }
561                        
562                } catch (Exception e) {
563                        Loggers.SQL_LOG.error("[IS0117] {}: {}", e.getMessage(), e);
564                        throw new PersistenceException(e.getMessage(), e);
565                } finally {
566                        timerCtx.stop();
567                }
568        }
569        
570        
571        @Override
572        public Publisher<MarshalledEntry<K, V>> publishEntries(Predicate<? super K> filter, boolean fetchValue, boolean fetchMetadata) {
573                
574                Loggers.SQL_LOG.trace("[IS0118] SQL store: Processing key filter for {} cache: fetchValue={} fetchMetadata={}",
575                        getCacheName(), fetchValue, fetchMetadata);
576                
577                final Instant now = Instant.now();
578                
579                return Flowable.using(timers.processTimer::time,
580                        ignore -> Flowable.fromIterable(sql.selectFrom(table(sqlRecordTransformer.getTableName())).fetch())
581                                .map(sqlRecordTransformer::toInfinispanEntry)
582                                .filter(infinispanEntry -> filter == null || filter.test(infinispanEntry.getKey()))
583                                .filter(infinispanEntry -> ! infinispanEntry.isExpired(now))
584                                .map(infinispanEntry -> marshalledEntryFactory.newMarshalledEntry(
585                                        infinispanEntry.getKey(),
586                                        infinispanEntry.getValue(),
587                                        infinispanEntry.getMetadata()))
588                                .doOnError(e -> Loggers.SQL_LOG.error("[IS0119] {}: {}", e.getMessage(), e)),
589                        Timer.Context::stop);
590        }
591
592
593        @Override
594        public int size() {
595
596                // Infinispan code analysis on 8.2 shows that this method is never called in practice, and
597                // is not wired to the data / cache container API
598
599                Loggers.SQL_LOG.trace("[IS0120] SQL store: Counting {} records", getCacheName());
600
601                final int count;
602                
603                try {
604                        count = sql.fetchCount(table(sqlRecordTransformer.getTableName()));
605                        
606                } catch (Exception e) {
607                        Loggers.SQL_LOG.error("[IS0121] {}: {}", e.getMessage(), e);
608                        throw new PersistenceException(e.getMessage(), e);
609                }
610
611                Loggers.SQL_LOG.trace("[IS0122] SQL store: Counted {} {} records", count, getCacheName());
612
613                return count;
614        }
615
616
617        @Override
618        public void clear() {
619
620                Loggers.SQL_LOG.trace("[IS0123] SQL store: Clearing {} records", getCacheName());
621
622                int numDeleted;
623                
624                try {
625                        numDeleted = sql.deleteFrom(table(sqlRecordTransformer.getTableName())).execute();
626                        
627                } catch (Exception e) {
628                        Loggers.SQL_LOG.error("[IS0124] {}: {}", e.getMessage(), e);
629                        throw new PersistenceException(e.getMessage(), e);
630                }
631
632                Loggers.SQL_LOG.info("[IS0125] SQL store: Cleared {} {} records", numDeleted, sqlRecordTransformer.getTableName());
633        }
634
635
636        @Override
637        public void purge(final Executor executor, final PurgeListener<? super K> purgeListener) {
638
639                // Should never be called in the presence of purge(Executor,ExpirationPurgeListener)
640                
641                Loggers.SQL_LOG.trace("[IS0126] SQL store: Purging {} cache entries", getCacheName());
642                
643                Timer.Context timerCtx = timers.purgeTimer.time();
644
645                try {
646                        executor.execute(() -> reaper.purge(purgeListener));
647                        
648                } catch (Exception e) {
649                        Loggers.SQL_LOG.error("[IS0127] {}: {}", e.getMessage(), e);
650                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
651                } finally {
652                        timerCtx.stop();
653                }
654        }
655        
656        
657        @Override
658        public void purge(final Executor executor, final ExpirationPurgeListener<K,V> purgeListener) {
659                
660                Loggers.SQL_LOG.trace("[IS0150] SQL store: Purging {} cache entries", getCacheName());
661                
662                Timer.Context timerCtx = timers.purgeTimer.time();
663                
664                try {
665                        executor.execute(() -> reaper.purgeExtended(purgeListener));
666                        
667                } catch (Exception e) {
668                        Loggers.SQL_LOG.error("[IS0151] {}: {}", e.getMessage(), e);
669                        throw new PersistenceException("Purge exception: " + e.getMessage(), e);
670                } finally {
671                        timerCtx.stop();
672                }
673        }
674}