package gw.solr.consistency uses com.guidewire.pl.system.dependency.PLDependencies uses gw.api.system.PLLoggerCategory uses gw.entity.IEntityType uses gw.lang.reflect.TypeSystem uses gw.pl.persistence.core.Key uses gw.pl.util.JSONMultiObject uses gw.plugin.management.SolrMessageMonitor uses gw.solr.utils.ConsistencyAction uses gw.solr.utils.EntityKey uses gw.solr.utils.JSONMultiKeyParser uses gw.solr.utils.PLSolrUtils uses gw.solr.utils.TimedLRUCache uses org.apache.solr.client.solrj.request.UpdateRequest uses org.json.simple.JSONArray uses org.json.simple.JSONAware uses org.json.simple.JSONObject uses java.lang.IllegalStateException uses java.lang.Integer uses java.lang.Long uses java.time.Duration uses java.util.ArrayList uses java.util.HashMap uses java.util.Map /** * Consistency tracking mechanism. Caches recent update messages for a Solr * document so that if a subsequent message has used invalid data (typically * due to stale reads because of late or missed cached evictions), then the * message can be reconstructed from the proper data and sent along. */ @ReadOnly final class ConsistencyTracker { final static var ENTITY_NAMESPACE = "entity" final var _coreFinder = PLDependencies.getCoreFinder() final var _globalCache = PLDependencies.getGlobalCache() final var _consistencyCache : TimedLRUCache final var _reconstructor : IReconstructor final var _keyGenerator : IKeyGenerator final var _expireDuration : Duration as readonly ExpireDuration var _enabled : boolean as Enabled = true construct(reconstructor : IReconstructor, cache : TimedLRUCache, expireDuration : Duration) { this(reconstructor, new UrnKeyGenerator(), cache, expireDuration) } construct(reconstructor : IReconstructor, keyGenerator : IKeyGenerator, cache : TimedLRUCache, expireDuration : Duration) { _reconstructor = reconstructor _keyGenerator = keyGenerator _consistencyCache = cache _expireDuration = expireDuration } final function enable() { Enabled = true } final function disable() { Enabled = false } function maybeCorrectMessage(final msg : String) : UpdateRequest { var processor : ConsistencyProcessor var jsonParser = new JSONMultiKeyParser() var msgObj = jsonParser.parse(msg) if(msgObj typeis JSONArray) { processor = new UpdateProcessor(msgObj, this) SolrMessageMonitor.getInstance().incrementTotalUpdatedDocuments(msgObj.size()) } else if(msgObj typeis JSONMultiObject) { processor = new DeleteProcessor(msgObj, this) SolrMessageMonitor.getInstance().incrementTotalDeletes() } else if(msgObj != null) { throw new IllegalStateException( "Solr message parsed as wrong type. Expected JSONArray or JSONMultiObject. Got " + (typeof msgObj).Name) } return processor != null ? processor.process() : null } function createCacheKey(docObj : JSONObject, urnTag : String) : String { var key = _keyGenerator.createKey(docObj, urnTag) return key } function findConsistencyInfo(key : String, parser : JSONMultiKeyParser) : JSONObject { var info = _consistencyCache.findEntry(key) return parser.parse(info) } function storeConsistencyInfo(oldKey : String, newKey : String, infoObj : JSONAware) { if(oldKey != newKey) { if(PLLoggerCategory.SOLR_INDEX.TraceEnabled) { PLLoggerCategory.SOLR_INDEX.trace("Removing old consistency record for {} which was {}", oldKey, _consistencyCache.findEntry(oldKey)) } _consistencyCache.removeEntry(oldKey) } if(newKey != null) { if(PLLoggerCategory.SOLR_INDEX.TraceEnabled) { PLLoggerCategory.SOLR_INDEX.trace("Storing new consistency record for {} which is {}", newKey, infoObj) } _consistencyCache.storeEntry(newKey, infoObj?.toJSONString()) } } function checkAndUpdateVersion(docObj : JSONObject, newInfo : JSONObject, oldInfo : JSONObject) : JSONObject { var result = docObj var newVersion = newInfo.get(PLSolrUtils.VERSIONS_TAG) as JSONArray var oldVersion = oldInfo.get(PLSolrUtils.VERSIONS_TAG) as JSONArray // XXX else cases assume SOLR_DELETE operation, nothing else supported right now var updatedVersions = new HashMap() if(PLSolrUtils.SOLR_UPDATE.equals(oldInfo.get(PLSolrUtils.OPERATION_TAG))) { if(PLSolrUtils.SOLR_UPDATE.equals(newInfo.get(PLSolrUtils.OPERATION_TAG))) { // old = update, new = update if(isOutdatedUpdate(convert(oldVersion), convert(newVersion), updatedVersions) == RECONSTRUCT) { PLLoggerCategory.SOLR_INDEX.info("Solr update inconsistency found for record, urn={}. Reconstructing update message.", docObj.get("urn")) result = reconstructMessage(docObj, updatedVersions) } } else { // old = update, new = delete var choice = isOutdatedDelete(convert(newVersion), convert(oldVersion), updatedVersions, false) switch(choice) { case IGNORE: PLLoggerCategory.SOLR_INDEX.info("Solr update inconsistency found for record, urn={}. Ignoring delete of updated record.", docObj.get("urn")) result = null break case RECONSTRUCT: PLLoggerCategory.SOLR_INDEX.info("Solr update inconsistency found for record, urn={}. Reconstructing update message.", docObj.get("urn")) result = reconstructMessage(docObj, updatedVersions) break case NOCHANGE: break } } } else { if(PLSolrUtils.SOLR_UPDATE.equals(newInfo.get(PLSolrUtils.OPERATION_TAG))) { // old = delete, new = update var choice = isOutdatedDelete(convert(oldVersion), convert(newVersion), updatedVersions, true) switch(choice) { case IGNORE: PLLoggerCategory.SOLR_INDEX.info("Solr update inconsistency found for record, urn={}. Ignoring update of deleted record.", docObj.get("urn")) result = null break case RECONSTRUCT: PLLoggerCategory.SOLR_INDEX.info("Solr update inconsistency found for record, urn={}. Reconstructing update message.", docObj.get("urn")) result = reconstructMessage(docObj, updatedVersions) break case NOCHANGE: break } } else { // old = delete, new = delete // no inconsistency for this case, could ignore it if we wanted to... } } return result } function isOutdatedDelete(deleteVersion : Map, updateVersion : Map, updated : Map, deletePrecedesUpdate : boolean) : ConsistencyAction { var result : ConsistencyAction = IGNORE if(deleteVersion != null && updateVersion != null) { var updateContainsOlder = false var updateContainsNewer = false if(PLLoggerCategory.SOLR_INDEX.TraceEnabled) { PLLoggerCategory.SOLR_INDEX.trace("Comparing delete {} against update {}", deleteVersion, updateVersion) } for(de in deleteVersion.entrySet()) { var uev = updateVersion.get(de.Key) if(uev != null) { switch(uev.compareTo(de.Value)) { case -1: updateContainsOlder = true break case 1: updateContainsNewer = true break } } else { // XXX Implies the URN's aren't really the same, which should never happen, // but return NOCHANGE, just in case result = NOCHANGE } } if(deletePrecedesUpdate) { if(updateContainsNewer) { result = updateContainsOlder ? RECONSTRUCT : NOCHANGE } else { // IGNORE (or inherit NOCHANGE from null case, above) } } else { if(updateContainsNewer) { // suppress delete action result = IGNORE } else { // accept delete action result = NOCHANGE } } if(result == RECONSTRUCT) { for(ue in updateVersion.entrySet()) { if(updated.get(ue.Key) == null) { updated.put(ue.Key, ue.Value) } } } } return result } function isOutdatedUpdate(oldVersion : Map, newVersion : Map, updated : Map) : ConsistencyAction { var result : ConsistencyAction = NOCHANGE if(oldVersion != null && newVersion != null) { if(PLLoggerCategory.SOLR_INDEX.TraceEnabled) { PLLoggerCategory.SOLR_INDEX.trace("Comparing old {} against new {}", oldVersion, newVersion) } for(oe in oldVersion.entrySet()) { var nev = newVersion.get(oe.Key) if(nev == null || nev.compareTo(oe.Value) < 0) { updated.put(oe.Key, oe.Value) result = RECONSTRUCT } else { updated.put(oe.Key, nev) } } if(result == RECONSTRUCT) { for(ne in newVersion.entrySet()) { if(updated.get(ne.Key) == null) { updated.put(ne.Key, ne.Value) } } } } return result } function convert(eva : JSONArray) : Map { var result = new HashMap() eva?.each( \ e : Object -> result.put(new EntityKey(e as JSONObject), (e as JSONObject).get(PLSolrUtils.BEAN_VERSION_TAG) as Integer)) return result } function reconstructMessage(docObj : JSONObject, beanVersions : Map) : JSONObject { var updatedBeans = new ArrayList() for(e in beanVersions.entrySet()) { var entityType = TypeSystem.getByFullNameIfValid(ENTITY_NAMESPACE + "." + e.Key.EntityType) as IEntityType var key = new Key(entityType, Long.parseLong(e.Key.ID)) // !PW Remove optimization where we only evicted changed beans. Some // reconstructions weren't picking up the changes unless we evicted all // related beans, changed or not. _globalCache.evictBean(key) updatedBeans.add(entityByPublicId(entityType, e.Key.PublicID)) } return _reconstructor.reconstructDocument(docObj, updatedBeans) } reified function entityByPublicId(entityType : IEntityType, publicID : String) : T { return _coreFinder.baseFindByPublicId(entityType, publicID) as T } }