Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.79% covered (success)
91.79%
246 / 268
88.24% covered (warning)
88.24%
15 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserCache
91.79% covered (success)
91.79%
246 / 268
88.24% covered (warning)
88.24%
15 / 17
61.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 setFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteOptionsKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getDirty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getContentModelFromPage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 incrementStats
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 incrementRenderReasonStats
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getMetadata
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
7
 makeMetadataKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeParserOutputKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
12
 save
86.84% covered (warning)
86.84%
99 / 114
0.00% covered (danger)
0.00%
0 / 1
17.66
 getCacheStorage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkExpired
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 checkOutdated
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 restoreFromJson
56.25% covered (warning)
56.25%
9 / 16
0.00% covered (danger)
0.00%
0 / 1
3.75
 convertForCache
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Cache for outputs of the PHP parser
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Cache Parser
22 */
23
24use MediaWiki\HookContainer\HookContainer;
25use MediaWiki\HookContainer\HookRunner;
26use MediaWiki\Json\JsonCodec;
27use MediaWiki\Page\PageRecord;
28use MediaWiki\Page\WikiPageFactory;
29use MediaWiki\Parser\ParserCacheFilter;
30use MediaWiki\Parser\ParserCacheMetadata;
31use MediaWiki\Parser\ParserOutput;
32use MediaWiki\Title\TitleFactory;
33use Psr\Log\LoggerInterface;
34use Wikimedia\Stats\StatsFactory;
35use Wikimedia\UUID\GlobalIdGenerator;
36
37/**
38 * Cache for ParserOutput objects corresponding to the latest page revisions.
39 *
40 * The ParserCache is a two-tiered cache backed by BagOStuff which supports
41 * varying the stored content on the values of ParserOptions used during
42 * a page parse.
43 *
44 * First tier is keyed by the page ID and stores ParserCacheMetadata, which
45 * contains information about cache expiration and the list of ParserOptions
46 * used during the parse of the page. For example, if only 'dateformat' and
47 * 'userlang' options were accessed by the parser when producing output for the
48 * page, array [ 'dateformat', 'userlang' ] will be stored in the metadata cache.
49 * This means none of the other existing options had any effect on the output.
50 *
51 * The second tier of the cache contains ParserOutput objects. The key for the
52 * second tier is constructed from the page ID and values of those ParserOptions
53 * used during a page parse which affected the output. Upon cache lookup, the list
54 * of used option names is retrieved from tier 1 cache, and only the values of
55 * those options are hashed together with the page ID to produce a key, while
56 * the rest of the options are ignored. Following the example above where
57 * only [ 'dateformat', 'userlang' ] options changed the parser output for a
58 * page, the key will look like 'page_id!dateformat=default:userlang=ru'.
59 * Thus any cache lookup with dateformat=default and userlang=ru will hit the
60 * same cache entry regardless of the values of the rest of the options, since they
61 * were not accessed during a parse and thus did not change the output.
62 *
63 * @see ParserOutput::recordOption()
64 * @see ParserOutput::getUsedOptions()
65 * @see ParserOptions::allCacheVaryingOptions()
66 * @ingroup Cache Parser
67 */
68class ParserCache {
69    /**
70     * Constants for self::getKey()
71     * @since 1.30
72     * @since 1.36 the constants were made public
73     */
74
75    /** Use only current data */
76    public const USE_CURRENT_ONLY = 0;
77
78    /** Use expired data if current data is unavailable */
79    public const USE_EXPIRED = 1;
80
81    /** Use expired data or data from different revisions if current data is unavailable */
82    public const USE_OUTDATED = 2;
83
84    /**
85     * Use expired data and data from different revisions, and if all else
86     * fails vary on all variable options
87     */
88    private const USE_ANYTHING = 3;
89
90    /** @var string The name of this ParserCache. Used as a root of the cache key. */
91    private $name;
92
93    /** @var BagOStuff */
94    private $cache;
95
96    /**
97     * Anything cached prior to this is invalidated
98     *
99     * @var string
100     */
101    private $cacheEpoch;
102
103    /** @var HookRunner */
104    private $hookRunner;
105
106    /** @var JsonCodec */
107    private $jsonCodec;
108
109    /** @var StatsFactory */
110    private $stats;
111
112    /** @var LoggerInterface */
113    private $logger;
114
115    /** @var TitleFactory */
116    private $titleFactory;
117
118    /** @var WikiPageFactory */
119    private $wikiPageFactory;
120
121    private ?ParserCacheFilter $filter = null;
122
123    private GlobalIdGenerator $globalIdGenerator;
124
125    /**
126     * @var BagOStuff small in-process cache to store metadata.
127     * It's needed multiple times during the request, for example
128     * to build a PoolWorkArticleView key, and then to fetch the
129     * actual ParserCache entry.
130     */
131    private $metadataProcCache;
132
133    /**
134     * Setup a cache pathway with a given back-end storage mechanism.
135     *
136     * This class use an invalidation strategy that is compatible with
137     * MultiWriteBagOStuff in async replication mode.
138     *
139     * @param string $name
140     * @param BagOStuff $cache
141     * @param string $cacheEpoch Anything before this timestamp is invalidated
142     * @param HookContainer $hookContainer
143     * @param JsonCodec $jsonCodec
144     * @param StatsFactory $stats
145     * @param LoggerInterface $logger
146     * @param TitleFactory $titleFactory
147     * @param WikiPageFactory $wikiPageFactory
148     * @param GlobalIdGenerator $globalIdGenerator
149     */
150    public function __construct(
151        string $name,
152        BagOStuff $cache,
153        string $cacheEpoch,
154        HookContainer $hookContainer,
155        JsonCodec $jsonCodec,
156        StatsFactory $stats,
157        LoggerInterface $logger,
158        TitleFactory $titleFactory,
159        WikiPageFactory $wikiPageFactory,
160        GlobalIdGenerator $globalIdGenerator
161    ) {
162        $this->name = $name;
163        $this->cache = $cache;
164        $this->cacheEpoch = $cacheEpoch;
165        $this->hookRunner = new HookRunner( $hookContainer );
166        $this->jsonCodec = $jsonCodec;
167        $this->stats = $stats;
168        $this->logger = $logger;
169        $this->titleFactory = $titleFactory;
170        $this->wikiPageFactory = $wikiPageFactory;
171        $this->globalIdGenerator = $globalIdGenerator;
172        $this->metadataProcCache = new HashBagOStuff( [ 'maxKeys' => 2 ] );
173    }
174
175    /**
176     * @since 1.41
177     * @param ParserCacheFilter $filter
178     */
179    public function setFilter( ParserCacheFilter $filter ): void {
180        $this->filter = $filter;
181    }
182
183    /**
184     * @param PageRecord $page
185     * @since 1.28
186     */
187    public function deleteOptionsKey( PageRecord $page ) {
188        $page->assertWiki( PageRecord::LOCAL );
189        $key = $this->makeMetadataKey( $page );
190        $this->metadataProcCache->delete( $key );
191        $this->cache->delete( $key );
192    }
193
194    /**
195     * Retrieve the ParserOutput from ParserCache, even if it's outdated.
196     * @param PageRecord $page
197     * @param ParserOptions $popts
198     * @return ParserOutput|false
199     */
200    public function getDirty( PageRecord $page, $popts ) {
201        $page->assertWiki( PageRecord::LOCAL );
202        $value = $this->get( $page, $popts, true );
203        return is_object( $value ) ? $value : false;
204    }
205
206    /**
207     * @param PageRecord $page
208     * @return string
209     */
210    private function getContentModelFromPage( PageRecord $page ) {
211        $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
212        return str_replace( '.', '_', $wikiPage->getContentModel() );
213    }
214
215    /**
216     * @param PageRecord $page
217     * @param string $status
218     * @param string|null $reason
219     */
220    private function incrementStats( PageRecord $page, $status, $reason = null ) {
221        $contentModel = $this->getContentModelFromPage( $page );
222        $metricSuffix = $reason ? "{$status}_{$reason}" : $status;
223
224        $this->stats->getCounter( 'ParserCache_operation_total' )
225            ->setLabel( 'name', $this->name )
226            ->setLabel( 'contentModel', $contentModel )
227            ->setLabel( 'status', $status )
228            ->setLabel( 'reason', $reason ?: 'n/a' )
229            ->copyToStatsdAt( "{$this->name}.{$contentModel}.{$metricSuffix}" )
230            ->increment();
231    }
232
233    /**
234     * @param PageRecord $page
235     * @param string $renderReason
236     */
237    private function incrementRenderReasonStats( PageRecord $page, $renderReason ) {
238        $contentModel = $this->getContentModelFromPage( $page );
239        $renderReason = preg_replace( '/\W+/', '_', $renderReason );
240
241        $this->stats->getCounter( 'ParserCache_render_total' )
242            ->setLabel( 'name', $this->name )
243            ->setLabel( 'contentModel', $contentModel )
244            ->setLabel( 'reason', $renderReason )
245            ->copyToStatsdAt( "{$this->name}.{$contentModel}.reason.{$renderReason}" )
246            ->increment();
247    }
248
249    /**
250     * Returns the ParserCache metadata about the given page
251     * considering the given options.
252     *
253     * @note Which parser options influence the cache key
254     * is controlled via ParserOutput::recordOption() or
255     * ParserOptions::addExtraKey().
256     *
257     * @param PageRecord $page
258     * @param int $staleConstraint one of the self::USE_ constants
259     * @return ParserCacheMetadata|null
260     * @since 1.36
261     */
262    public function getMetadata(
263        PageRecord $page,
264        int $staleConstraint = self::USE_ANYTHING
265    ): ?ParserCacheMetadata {
266        $page->assertWiki( PageRecord::LOCAL );
267
268        $pageKey = $this->makeMetadataKey( $page );
269        $metadata = $this->metadataProcCache->get( $pageKey );
270        if ( !$metadata ) {
271            $metadata = $this->cache->get(
272                $pageKey,
273                BagOStuff::READ_VERIFIED
274            );
275        }
276
277        if ( $metadata === false ) {
278            $this->incrementStats( $page, 'miss', 'absent_metadata' );
279            $this->logger->debug( 'ParserOutput metadata cache miss', [ 'name' => $this->name ] );
280            return null;
281        }
282
283        // NOTE: If the value wasn't serialized to JSON when being stored,
284        //       we may already have a ParserOutput object here. This used
285        //       to be the default behavior before 1.36. We need to retain
286        //       support so we can handle cached objects after an update
287        //       from an earlier revision.
288        // NOTE: Support for reading string values from the cache must be
289        //       deployed a while before starting to write JSON to the cache,
290        //       in case we have to revert either change.
291        if ( is_string( $metadata ) ) {
292            $metadata = $this->restoreFromJson( $metadata, $pageKey, CacheTime::class );
293        }
294
295        if ( !$metadata instanceof CacheTime ) {
296            $this->incrementStats( $page, 'miss', 'unserialize' );
297            return null;
298        }
299
300        if ( $this->checkExpired( $metadata, $page, $staleConstraint, 'metadata' ) ) {
301            return null;
302        }
303
304        if ( $this->checkOutdated( $metadata, $page, $staleConstraint, 'metadata' ) ) {
305            return null;
306        }
307
308        $this->logger->debug( 'Parser cache options found', [ 'name' => $this->name ] );
309        return $metadata;
310    }
311
312    /**
313     * @param PageRecord $page
314     * @return string
315     */
316    private function makeMetadataKey( PageRecord $page ): string {
317        return $this->cache->makeKey( $this->name, 'idoptions', $page->getId( PageRecord::LOCAL ) );
318    }
319
320    /**
321     * Get a key that will be used by the ParserCache to store the content
322     * for a given page considering the given options and the array of
323     * used options.
324     *
325     * @warning The exact format of the key is considered internal and is subject
326     * to change, thus should not be used as storage or long-term caching key.
327     * This is intended to be used for logging or keying something transient.
328     *
329     * @param PageRecord $page
330     * @param ParserOptions $options
331     * @param array|null $usedOptions Defaults to all cache varying options.
332     * @return string
333     * @internal
334     * @since 1.36
335     */
336    public function makeParserOutputKey(
337        PageRecord $page,
338        ParserOptions $options,
339        array $usedOptions = null
340    ): string {
341        $usedOptions ??= ParserOptions::allCacheVaryingOptions();
342        // idhash seem to mean 'page id' + 'rendering hash' (r3710)
343        $pageid = $page->getId( PageRecord::LOCAL );
344        $title = $this->titleFactory->newFromPageIdentity( $page );
345        $hash = $options->optionsHash( $usedOptions, $title );
346        // Before T263581 ParserCache was split between normal page views
347        // and action=parse. -0 is left in the key to avoid invalidating the entire
348        // cache when removing the cache split.
349        return $this->cache->makeKey( $this->name, 'idhash', "{$pageid}-0!{$hash}" );
350    }
351
352    /**
353     * Retrieve the ParserOutput from ParserCache.
354     * false if not found or outdated.
355     *
356     * @param PageRecord $page
357     * @param ParserOptions $popts
358     * @param bool $useOutdated (default false)
359     *
360     * @return ParserOutput|false
361     */
362    public function get( PageRecord $page, $popts, $useOutdated = false ) {
363        $page->assertWiki( PageRecord::LOCAL );
364
365        if ( !$page->exists() ) {
366            $this->incrementStats( $page, 'miss', 'nonexistent' );
367            return false;
368        }
369
370        if ( $page->isRedirect() ) {
371            // It's a redirect now
372            $this->incrementStats( $page, 'miss', 'redirect' );
373            return false;
374        }
375
376        $staleConstraint = $useOutdated ? self::USE_OUTDATED : self::USE_CURRENT_ONLY;
377        $parserOutputMetadata = $this->getMetadata( $page, $staleConstraint );
378        if ( !$parserOutputMetadata ) {
379            return false;
380        }
381
382        if ( !$popts->isSafeToCache( $parserOutputMetadata->getUsedOptions() ) ) {
383            $this->incrementStats( $page, 'miss', 'unsafe' );
384            return false;
385        }
386
387        $parserOutputKey = $this->makeParserOutputKey(
388            $page,
389            $popts,
390            $parserOutputMetadata->getUsedOptions()
391        );
392
393        $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
394        if ( $value === false ) {
395            $this->incrementStats( $page, 'miss', 'absent' );
396            $this->logger->debug( 'ParserOutput cache miss', [ 'name' => $this->name ] );
397            return false;
398        }
399
400        // NOTE: If the value wasn't serialized to JSON when being stored,
401        //       we may already have a ParserOutput object here. This used
402        //       to be the default behavior before 1.36. We need to retain
403        //       support so we can handle cached objects after an update
404        //       from an earlier revision.
405        // NOTE: Support for reading string values from the cache must be
406        //       deployed a while before starting to write JSON to the cache,
407        //       in case we have to revert either change.
408        if ( is_string( $value ) ) {
409            $value = $this->restoreFromJson( $value, $parserOutputKey, ParserOutput::class );
410        }
411
412        if ( !$value instanceof ParserOutput ) {
413            $this->incrementStats( $page, 'miss', 'unserialize' );
414            return false;
415        }
416
417        if ( $this->checkExpired( $value, $page, $staleConstraint, 'output' ) ) {
418            return false;
419        }
420
421        if ( $this->checkOutdated( $value, $page, $staleConstraint, 'output' ) ) {
422            return false;
423        }
424
425        $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
426        if ( $this->hookRunner->onRejectParserCacheValue( $value, $wikiPage, $popts ) === false ) {
427            $this->incrementStats( $page, 'miss', 'rejected' );
428            $this->logger->debug( 'key valid, but rejected by RejectParserCacheValue hook handler',
429                [ 'name' => $this->name ] );
430            return false;
431        }
432
433        $this->logger->debug( 'ParserOutput cache found', [ 'name' => $this->name ] );
434        $this->incrementStats( $page, 'hit' );
435        return $value;
436    }
437
438    /**
439     * @param ParserOutput $parserOutput
440     * @param PageRecord $page
441     * @param ParserOptions $popts
442     * @param string|null $cacheTime TS_MW timestamp when the cache was generated
443     * @param int|null $revId Revision ID that was parsed
444     */
445    public function save(
446        ParserOutput $parserOutput,
447        PageRecord $page,
448        $popts,
449        $cacheTime = null,
450        $revId = null
451    ) {
452        $page->assertWiki( PageRecord::LOCAL );
453        // T350538: Eventually we'll warn if the $cacheTime and $revId
454        // parameters are non-null here, since we *should* be getting
455        // them from the ParserOutput.
456        if ( $revId !== null && $revId !== $parserOutput->getCacheRevisionId() ) {
457            $this->logger->warning(
458                'Inconsistent revision ID',
459                [
460                    'name' => $this->name,
461                    'reason' => $popts->getRenderReason(),
462                    'revid1' => $revId,
463                    'revid2' => $parserOutput->getCacheRevisionId(),
464                ]
465            );
466        }
467
468        if ( !$parserOutput->hasText() ) {
469            throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
470        }
471
472        $expire = $parserOutput->getCacheExpiry();
473
474        if ( !$popts->isSafeToCache( $parserOutput->getUsedOptions() ) ) {
475            $this->logger->debug(
476                'Parser options are not safe to cache and has not been saved',
477                [ 'name' => $this->name ]
478            );
479            $this->incrementStats( $page, 'save', 'unsafe' );
480            return;
481        }
482
483        if ( $expire <= 0 ) {
484            $this->logger->debug(
485                'Parser output was marked as uncacheable and has not been saved',
486                [ 'name' => $this->name ]
487            );
488            $this->incrementStats( $page, 'save', 'uncacheable' );
489            return;
490        }
491
492        if ( $this->filter && !$this->filter->shouldCache( $parserOutput, $page, $popts ) ) {
493            $this->logger->debug(
494                'Parser output was filtered and has not been saved',
495                [ 'name' => $this->name ]
496            );
497            $this->incrementStats( $page, 'save', 'filtered' );
498
499            // TODO: In this case, we still want to cache in RevisionOutputCache (T350669).
500            return;
501        }
502
503        if ( $this->cache instanceof EmptyBagOStuff ) {
504            return;
505        }
506
507        // Ensure cache properties are set in the ParserOutput
508        // T350538: These should be turned into assertions that the
509        // properties are already present.
510        if ( $cacheTime ) {
511            $parserOutput->setCacheTime( $cacheTime );
512        } else {
513            if ( !$parserOutput->hasCacheTime() ) {
514                $this->logger->warning(
515                    'No cache time set',
516                    [
517                        'name' => $this->name,
518                        'reason' => $popts->getRenderReason(),
519                    ]
520                );
521            }
522            $cacheTime = $parserOutput->getCacheTime();
523        }
524
525        if ( $revId ) {
526            $parserOutput->setCacheRevisionId( $revId );
527        } elseif ( $parserOutput->getCacheRevisionId() ) {
528            $revId = $parserOutput->getCacheRevisionId();
529        } else {
530            $revId = $page->getLatest( PageRecord::LOCAL );
531            $parserOutput->setCacheRevisionId( $revId );
532        }
533        if ( !$revId ) {
534            $this->logger->warning(
535                'Parser output cannot be saved if the revision ID is not known',
536                [ 'name' => $this->name ]
537            );
538            $this->incrementStats( $page, 'save', 'norevid' );
539            return;
540        }
541
542        if ( !$parserOutput->getRenderId() ) {
543            $this->logger->warning(
544                'Parser output missing render ID',
545                [
546                    'name' => $this->name,
547                    'reason' => $popts->getRenderReason(),
548                ]
549            );
550            $parserOutput->setRenderId( $this->globalIdGenerator->newUUIDv1() );
551        }
552
553        // Transfer cache properties to the cache metadata
554        $metadata = new CacheTime;
555        $metadata->recordOptions( $parserOutput->getUsedOptions() );
556        $metadata->updateCacheExpiry( $expire );
557        $metadata->setCacheTime( $cacheTime );
558        $metadata->setCacheRevisionId( $revId );
559
560        $parserOutputKey = $this->makeParserOutputKey(
561            $page,
562            $popts,
563            $metadata->getUsedOptions()
564        );
565
566        $msg = "Saved in parser cache with key $parserOutputKey" .
567            " and timestamp $cacheTime" .
568            " and revision id $revId.";
569
570        $reason = $popts->getRenderReason();
571        $msg .= " Rendering was triggered because: $reason";
572
573        $parserOutput->addCacheMessage( $msg );
574
575        $pageKey = $this->makeMetadataKey( $page );
576
577        $parserOutputData = $this->convertForCache( $parserOutput, $parserOutputKey );
578        $metadataData = $this->convertForCache( $metadata, $pageKey );
579
580        if ( !$parserOutputData || !$metadataData ) {
581            $this->logger->warning(
582                'Parser output failed to serialize and was not saved',
583                [ 'name' => $this->name ]
584            );
585            $this->incrementStats( $page, 'save', 'nonserializable' );
586            return;
587        }
588
589        // Save the parser output
590        $this->cache->set(
591            $parserOutputKey,
592            $parserOutputData,
593            $expire,
594            BagOStuff::WRITE_ALLOW_SEGMENTS
595        );
596
597        // ...and its pointer to the local cache.
598        $this->metadataProcCache->set( $pageKey, $metadataData, $expire );
599        // ...and to the global cache.
600        $this->cache->set( $pageKey, $metadataData, $expire );
601
602        $title = $this->titleFactory->newFromPageIdentity( $page );
603        $this->hookRunner->onParserCacheSaveComplete( $this, $parserOutput, $title, $popts, $revId );
604
605        $this->logger->debug( 'Saved in parser cache', [
606            'name' => $this->name,
607            'key' => $parserOutputKey,
608            'cache_time' => $cacheTime,
609            'rev_id' => $revId
610        ] );
611        $this->incrementStats( $page, 'save', 'success' );
612        $this->incrementRenderReasonStats( $page, $popts->getRenderReason() );
613    }
614
615    /**
616     * Get the backend BagOStuff instance that
617     * powers the parser cache
618     *
619     * @since 1.30
620     * @internal
621     * @return BagOStuff
622     */
623    public function getCacheStorage() {
624        return $this->cache;
625    }
626
627    /**
628     * Check if $entry expired for $page given the $staleConstraint
629     * when fetching from $cacheTier.
630     * @param CacheTime $entry
631     * @param PageRecord $page
632     * @param int $staleConstraint One of USE_* constants.
633     * @param string $cacheTier
634     * @return bool
635     */
636    private function checkExpired(
637        CacheTime $entry,
638        PageRecord $page,
639        int $staleConstraint,
640        string $cacheTier
641    ): bool {
642        if ( $staleConstraint < self::USE_EXPIRED && $entry->expired( $page->getTouched() ) ) {
643            $this->incrementStats( $page, 'miss', 'expired' );
644            $this->logger->debug( "{$cacheTier} key expired", [
645                'name' => $this->name,
646                'touched' => $page->getTouched(),
647                'epoch' => $this->cacheEpoch,
648                'cache_time' => $entry->getCacheTime()
649            ] );
650            return true;
651        }
652        return false;
653    }
654
655    /**
656     * Check if $entry belongs to the latest revision of $page
657     * given $staleConstraint when fetched from $cacheTier.
658     * @param CacheTime $entry
659     * @param PageRecord $page
660     * @param int $staleConstraint One of USE_* constants.
661     * @param string $cacheTier
662     * @return bool
663     */
664    private function checkOutdated(
665        CacheTime $entry,
666        PageRecord $page,
667        int $staleConstraint,
668        string $cacheTier
669    ): bool {
670        $latestRevId = $page->getLatest( PageRecord::LOCAL );
671        if ( $staleConstraint < self::USE_OUTDATED && $entry->isDifferentRevision( $latestRevId ) ) {
672            $this->incrementStats( $page, 'miss', 'revid' );
673            $this->logger->debug( "{$cacheTier} key is for an old revision", [
674                'name' => $this->name,
675                'rev_id' => $latestRevId,
676                'cached_rev_id' => $entry->getCacheRevisionId()
677            ] );
678            return true;
679        }
680        return false;
681    }
682
683    /**
684     * @param string $jsonData
685     * @param string $key
686     * @param string $expectedClass
687     * @return CacheTime|ParserOutput|null
688     */
689    private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
690        try {
691            /** @var CacheTime $obj */
692            $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
693            return $obj;
694        } catch ( JsonException $e ) {
695            $this->logger->error( "Unable to unserialize JSON", [
696                'name' => $this->name,
697                'cache_key' => $key,
698                'message' => $e->getMessage()
699            ] );
700            return null;
701        } catch ( Exception $e ) {
702            $this->logger->error( "Unexpected failure during cache load", [
703                'name' => $this->name,
704                'cache_key' => $key,
705                'message' => $e->getMessage()
706            ] );
707            return null;
708        }
709    }
710
711    /**
712     * @param CacheTime $obj
713     * @param string $key
714     * @return string|null
715     */
716    protected function convertForCache( CacheTime $obj, string $key ) {
717        try {
718            return $this->jsonCodec->serialize( $obj );
719        } catch ( JsonException $e ) {
720            $this->logger->error( "Unable to serialize JSON", [
721                'name' => $this->name,
722                'cache_key' => $key,
723                'message' => $e->getMessage(),
724            ] );
725            return null;
726        }
727    }
728}