001    package org.hd.d.pg2k.ai.scorer;
002    
003    
004    import java.io.IOException;
005    import java.util.Set;
006    
007    import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
008    import org.hd.d.pg2k.svrCore.ExhibitName;
009    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
010    import org.hd.d.pg2k.svrCore.ExhibitThumbnails;
011    import org.hd.d.pg2k.svrCore.MemoryTools;
012    import org.hd.d.pg2k.svrCore.Name;
013    import org.hd.d.pg2k.svrCore.Name.ExhibitFull;
014    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
015    import org.hd.d.pg2k.svrCore.Tuple.Pair;
016    import org.hd.d.pg2k.svrCore.datasource.SimpleExhibitPipelineIF;
017    import org.hd.d.pg2k.svrCore.vars.EventPeriod;
018    
019    /**Shared abstract base to handle common Scorer cache tasks.
020     * Package-visible only for now to support initial cache implementations.
021     *
022     * @author dhd
023     */
024    abstract class AbstractScorerCache implements ScorerCacheIF
025        {
026        protected AbstractScorerCache(final ScorerPopulation population,
027                final SimpleExhibitPipelineIF dataSource,
028                final SimpleLoggerIF log)
029            {
030            if((population == null) || (log == null) || (dataSource == null))
031            { throw new IllegalArgumentException(); }
032            this.log = log;
033            this.population = population;
034            this.dataSource = dataSource;
035    
036            // We set up internal caches with capped sizes
037            // so as to concentrate memory usage on the population;
038            // the internal caches are scaled in proportion.
039    
040            // We also fold in the heap capacity as a consideration.
041    
042            // Limit the the raw score cache in proportion to the population size.
043            final int rMax = Math.max(2 * population.maxSize(), 2 * ScorerCreator.SUGGESTED_CALIB_SET_SIZE);
044            // Have a reduced lower limit to be whittled down to when memory is short.
045            // Experience suggests that a 1GB heap (2^30) should allow ~4k entries (2^12).
046            final int rMin = Math.max(1+ScorerCreator.MIN_SUGGESTED_CALIB_SET_SIZE,
047                    (int) Math.min(rMax/4, Runtime.getRuntime().totalMemory() >> 18));
048            _rawScores = MemoryTools.SimpleLRUMapAutoSizeForHitRate.<Pair<String,Name.ExhibitFull>, Pair<Long, ScoreAndConf>>create(rMin, rMax, "_rawScores");
049            }
050    
051        /**Logger; never null. */
052        protected final SimpleLoggerIF log;
053        /**Scorer population (and cache of Scorer "goodness"); never null. */
054        protected final ScorerPopulation population;
055        /**Live data source for variables, exhibit data and metadata, etc; never null. */
056        protected final SimpleExhibitPipelineIF dataSource;
057    
058        /**Make internal dataSource available to classes in the same package only; never null. */
059        SimpleExhibitPipelineIF getDataSource() { return(dataSource); }
060    
061        /**Get current population size; non-negative. */
062        public int size() { return(population.size()); }
063    
064        /**Make population available to classes in the same package only; never null. */
065        ScorerPopulation getPopulation() { return(population); }
066    
067        /**Poll to do incremental work; does nothing by default. */
068        public void poll() throws IOException { }
069    
070        /**Save work-in-progress if possible, and free up resources, ASAP.
071         * This may enable us to reduce work lost during a graceful system shutdown,
072         * but many shutdowns may not be graceful and so we should incrementally save/checkpoint too.
073         * <p>
074         * By default does nothing.
075         */
076        public void destroy() { }
077    
078        /**Current set of best-available Scorers with their parameters; never null but may be empty.
079         * This operation should always be relatively fast.
080         */
081        public Set<String> getCurrentScorersWithParameters(final boolean allowStale)
082            { return(population.getCurrentScorersWithParameters(allowStale)); }
083    
084        /**Size-limited cache of raw exhibit scores (and when computed); never null.
085         * Map from scorer name-and-parameters and exhibit name to ScoreAndConfidence and time computed.
086         * <p>
087         * This is a thread-safe map, with automatic LRU discarding of stale/unused values.
088         * <p>
089         * A lock can be held on this map to make compound operations atomic,
090         * though the lock should be held for as little time as possible
091         * so as to maximise available concurrency.
092         */
093        private final MemoryTools.SimpleLRUMapAutoSizeForHitRate<Pair<String,Name.ExhibitFull>, Pair<Long, ScoreAndConf>> _rawScores;
094    
095        /* Inherit javadoc. */
096        public ScoreAndConf computeUnweightedScoreAndConfidence(final ExhibitFull exhibitName, final ScorerIF scorer, final boolean allowStale)
097            throws IOException
098            {
099            if(scorer == null) { throw new IllegalArgumentException(); }
100            if(!ExhibitName.validNameSyntax(exhibitName)) { throw new IllegalArgumentException("exhibit name must be full, not short"); }
101    
102            final long startTime = System.currentTimeMillis();
103    
104            // Get vote events for all retained full/complete (VLONG) slots.
105            final long currentPeriod = EventPeriod.VLONG.getIntervalNumber(startTime);
106    
107            // If the exhibit is not valid/current then return the "no opinion" result.
108            final AllExhibitImmutableData aeid = dataSource.getAllExhibitImmutableData(-1);
109            final ExhibitStaticAttr esa = aeid.getStaticAttr(exhibitName);
110            if(esa == null) { return(ScoreAndConf.NO_OPINION); }
111    
112            // We can use a value from cache if the following conditions hold:
113            //  1) The cached value is from the current period
114            //     (or the previous period if allowStale is true).
115            //  2) The cached value is newer than the exhibit file date
116            //     (and any thumbnails that might be used).
117            // Since we expect thumbnails only to change rarely for a given exhibit,
118            // ie after a thumbnail-generation algorithm change,
119            // we certainly do not need to force thumbnail creation just to get a timestamp.
120            // We DO NOT intern() the name-and-parameters value because it would stress the PermGen.
121            // We assume that the exhibitName is already an intern()ed value.
122            final Pair<String, Name.ExhibitFull> cacheKey = new Pair<String, Name.ExhibitFull>(scorer.getNameAndParameters(), exhibitName);
123            // See what, if anything, is currently in cache.
124            final Pair<Long, ScoreAndConf> cachedValue = _rawScores.get(cacheKey);
125            final ExhibitThumbnails tns;
126            if((cachedValue != null) &&
127               ((EventPeriod.VLONG.getIntervalNumber(cachedValue.first) >= (allowStale ? currentPeriod-1 : currentPeriod)) &&
128               (cachedValue.first >= esa.timestamp) &&
129               (allowStale || (null == (tns = dataSource.getThumbnails(exhibitName, false))) || (cachedValue.first >= tns.created))))
130                { return(cachedValue.second); }
131    
132            // Compute value from scratch.
133            final ScoreAndConf result = scorer.computeScoreAndConfidence(dataSource, exhibitName);
134    
135            // Atomically replace the value in cache
136            // making sure that we never replace a newer value.
137            final Pair<Long, ScoreAndConf> newValue = new Pair<Long, ScoreAndConf>(startTime, result);
138            synchronized(_rawScores)
139                {
140                final Pair<Long, ScoreAndConf> curValue = _rawScores.get(cacheKey);
141                if((curValue == null) || (curValue.first < newValue.first))
142                    { _rawScores.put(cacheKey, newValue); }
143                }
144    
145            return(result);
146            }
147    
148        /**Default implementation is uncached; always returns null. */
149        public ScoreAndConf getCachedCompositeScoreAndConfidence(final ExhibitFull exhibitName, final boolean allowStale) throws IOException
150            { return(null); }
151    
152        /* (non-Javadoc)
153         * @see org.hd.d.pg2k.ai.scorer.ScorerCacheIF#computeScorerWeighting(org.hd.d.pg2k.ai.scorer.ScorerIF, boolean, java.lang.String)
154         */
155        public ScoreAndConf computeScorerWeighting(final String scorerNameAndParameters, final boolean allowStale, final String source) throws IOException
156            {
157            // Check that the scorer requested is available/valid.
158            // If not, return the "no opinion" result.
159            final ScorerIF scorer = getScorerInstance(scorerNameAndParameters);
160            if(scorer == null) { return(ScoreAndConf.NO_OPINION); }
161            return(computeScorerWeighting(scorer, allowStale, source));
162            }
163    
164        /* (non-Javadoc)
165         * @see org.hd.d.pg2k.ai.scorer.ScorerCacheIF#getScorerInstance(java.lang.String)
166         */
167        public ScorerIF getScorerInstance(final String nameAndParameters)
168            {
169            // Use the available functionality in the population instance.
170            return(population.getScorerInstance(nameAndParameters));
171            }
172    
173        /**Non-blocking attempt to queue an externally-supplied Scorer value; returns true if accepted.
174         * This default implementation always returns false.
175         */
176        public boolean offerExternalScorer(final String externalScorerNameAndParameters) { return(false); }
177    
178        /**Returns true if this cache can accept (many) more external-supplied Scorer values.
179         * This default implementation always returns false.
180         */
181        public boolean canAcceptMoreExternalScorers() { return(false); }
182    
183        /**Returns true if at least once external Scorer is queued waiting to be processed.
184         * This default implementation always returns false.
185         */
186        public boolean hasQueuedExternalScorer() { return(false); }
187        }