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