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