package gw.rating.rtm.query
uses com.google.common.base.Ticker
uses com.google.common.cache.CacheBuilder
uses com.google.common.cache.CacheLoader
uses com.google.common.cache.LoadingCache
uses com.google.common.cache.RemovalListener
uses com.guidewire.pl.system.cache.CacheChangeBatch
uses com.guidewire.pl.system.cache.GlobalCacheListener
uses gw.api.plugin.StartablePluginBase
uses gw.api.rating.cache.IRateTableRowCache
uses gw.api.startable.StartablePluginCallbackHandler
uses gw.api.startable.StartablePluginState
uses gw.api.system.PCLoggerCategory
uses gw.api.system.PLDependenciesGateway
uses gw.pl.persistence.core.Key
uses gw.rating.flow.util.QueryUtils
uses gw.rating.rtm.domain.OrderedPersistenceAdapter
uses java.util.concurrent.TimeUnit
uses java.util.stream.Stream
/**
* RateTableRow cache is required to be implemented as a global cache to account for cross cluster changes to rate
* tables.
*
* Note though that this is a cache implementation implemented as a plugin. This was done because
* GlobalCacheListeners are typically implemented as java methods and then initialized in the server init
* (@See PCInitTab.java), also written in java. However, RateTableRowCache is dependent on a lot of gosu code which made
* conversion to java more work then we were willing to take on at the moment. Plugins are intialized after Global Cache
* in the initialization life cycle and so we could achieve the same effect.
*
* Plugin must be marked @Distributed to ensure it is not created with an expirable lease. @See StartableServiceImp.startPluginsAtRunlevel()
*/
@ReadOnly
@Distributed
class RateTableRowCache extends StartablePluginBase implements IRateTableRowCache, GlobalCacheListener {
/**
* static config
*
* There's a tuning issue here: set this too low, and all rate tables will be flushed, say,
* over a weekend when nothing much happens. Set it too high, and no tables will ever be evicted.
*/
public static var CACHE_TIMEOUT : long = 36
public static var CACHE_TIMEOUT_UNIT : TimeUnit = TimeUnit.HOURS
private static var DEFAULT_CONCURRENCY = 16
construct() {
this(DEFAULT_CONCURRENCY, \notification -> {
PCLoggerCategory.RTM.debug("Removed ${notification}, reason ${notification.Cause}")
}, null)
}
/**
* Allow some callback extensibility for testing
*
* @param removalListener
* @param ticker
*/
protected construct(concurrency : Integer, removalListener : RemovalListener, ticker : Ticker) {
try {
PCLoggerCategory.RTM.info("Creating RTM row cache")
PCLoggerCategory.RTM.info("Cache timeout (in nanoseconds): " + CACHE_TIMEOUT_UNIT.toNanos(CACHE_TIMEOUT))
var builder = CacheBuilder.newBuilder().expireAfterAccess(CACHE_TIMEOUT, CACHE_TIMEOUT_UNIT)
if (concurrency != null) {
builder.concurrencyLevel(concurrency)
}
if (removalListener != null) {
builder.removalListener(removalListener)
}
if (ticker != null) {
builder.ticker(ticker)
}
_cache = builder.build(_cacheLoader)
} catch (e) {
PCLoggerCategory.RTM.error("Exception occurred in constructing rate table row cache", e)
throw e
}
}
/**
* Cache is implemented using Guava LoadingCache. Cache.get() calls getOrLoad backed by a default _cacheLoader.
* Cache is lazily loaded when someone requests for a specific table. Changes to rate tables are handled by the
* GlobalCacheListener method cacheChanged()
*/
private final var _cache : LoadingCache
/**
* restricted access to loadingcache to allow for some extensibility
*/
protected property get LoadingCache() : LoadingCache {
return _cache
}
/**
* lazily called by loadingcache getOrLoad()
*/
private final var _cacheLoader : CacheLoader = new CacheLoader() {
override function load(key : Key) : CacheEntry {
return new CacheEntry(key)
}
}
internal static class CacheEntry {
// map from NumberOfActiveMatchOps to applicable rows
final private var _rows : Map>as readonly Rows
final private var _date : java.util.Date as readonly Date
final private var _rowsNormal : Boolean as readonly Normalized
construct(tableID : Key) {
var table = QueryUtils.loadReadOnlyRateTableByKey(tableID)
PCLoggerCategory.RTM.info("Rate table cache miss: owning table ${table.TableOwningFactors.DisplayName}, ID: ${table.ID}")
// NOTE: Could extend here to allow for various persistence mechanisms
var loadedRows = new DatabaseRowLoader().loadRows(table)
var cachedPartitionedRowsMap : Map>
var rowsAreNormal : boolean
var ops = AbstractStatelessQuery.getMatchOperators(table.Definition)
try {
// Only normalize if it's not already normalized and we're willing to spend memory for perf
// (This also means we will not attempt to normalize if we know the table is TC_INTRACTABLE
var normalizeRows = table.RowUniformityStatus == TC_NONUNIFORM and !table.ReduceMemoryUsage
cachedPartitionedRowsMap = new StatelessRowLoadUtil()
.getPartitionedOrderedRowsMap(ops, loadedRows, normalizeRows)
rowsAreNormal = normalizeRows or table.RowUniformityStatus == TC_UNIFORM
} catch (pae : StatelessRowLoadUtil.PartitionAbortedException) {
cachedPartitionedRowsMap = new StatelessRowLoadUtil().getPartitionedOrderedRowsMap(ops, loadedRows, false)
rowsAreNormal = false
}
// make sure that get(0) returns something
if (not cachedPartitionedRowsMap.containsKey(0)) {
cachedPartitionedRowsMap.put(0, {})
}
_rows = Collections.unmodifiableMap(cachedPartitionedRowsMap)
_date = table.TableOwningFactors.UpdateTime
_rowsNormal = rowsAreNormal
PCLoggerCategory.RTM.info("Rate table cache populated: owning table ${table.TableOwningFactors.DisplayName}, ID: ${table.ID}")
}
}
/**
* @throws IllegalStateException if the requested table was updated without triggering the global cache listener. If used, all queries should go through this cache
*/
override function getCachedRows(table : RateTable) : CacheEntry {
var entry = _cache.get(table.TableOwningFactors.ID)
if (entry._date.before(table.TableOwningFactors.UpdateTime)) {
throw new java.lang.IllegalStateException("Cache entry for ${table.DisplayName} in book ${table.RateBook.DisplayName} is out of date and should have been evicted.")
}
return entry
}
/**
* Global cache listener methods
*/
override function cacheChanged(batch : CacheChangeBatch) {
// Search for global changes that are present in the rate table cache and evict them
if (batch.containsChangesFor(RateTable)) {
Stream.of(batch.getAllChangesFor(RateTable))
.filter(\elt -> _cache.asMap().containsKey(elt.VersionKey.Id))
.forEach(\elt -> {
PCLoggerCategory.RTM.debug("GlobalCache.cacheChanged() invoked. Evicting cache entry for ${elt.VersionKey.Id}")
_cache.invalidate(elt.VersionKey.Id);
})
}
}
override function flushCaches() {
invalidateAll()
}
override function invalidateAll() {
PCLoggerCategory.RTM.debug("RateTableRowCache.invalidateAll() invoked. Evicting cache entry for all rate tables")
_cache.invalidateAll()
}
override function resetStatistics() {
// statistics are not tracked for this cache
}
/**
* IStartablePlugin methods
*/
override function start(pluginCallbackHandler : StartablePluginCallbackHandler, serverStarting : boolean) {
super.start(pluginCallbackHandler, serverStarting)
PLDependenciesGateway.GlobalCache.addListener(this)
}
override function stop(serverShuttingDown : boolean) {
super.stop(serverShuttingDown)
PLDependenciesGateway.GlobalCache.removeListener(this)
invalidateAll()
}
override property get State() : StartablePluginState {
return PLDependenciesGateway.GlobalCache.isListener(this) ? StartablePluginState.Started : Stopped
}
}