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}