Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
CacheAbstractContentFragmentJob
100.00% covered (success)
100.00%
73 / 73
100.00% covered (success)
100.00%
5 / 5
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 run
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
6
 ignoreDuplicates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeduplicationInfo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 allowRetries
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * @file
5 * @ingroup Extensions
6 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
7 * @license MIT
8 */
9
10namespace MediaWiki\Extension\WikiLambda\Jobs;
11
12use MediaWiki\Config\Config;
13use MediaWiki\Extension\WikiLambda\AbstractContent\AbstractWikiRequest;
14use MediaWiki\Extension\WikiLambda\HttpStatus;
15use MediaWiki\Extension\WikiLambda\WikiLambdaServices;
16use MediaWiki\JobQueue\GenericParameterJob;
17use MediaWiki\JobQueue\Job;
18use MediaWiki\Logger\LoggerFactory;
19use MediaWiki\MediaWikiServices;
20use Psr\Log\LoggerInterface;
21use Wikimedia\Timestamp\ConvertibleTimestamp;
22use Wikimedia\Timestamp\TimestampFormat as TS;
23
24/**
25 * Asynchronous job run on Abstract Wiki to refresh an Abstract Content
26 * fragment which is only available in the cache in an older version.
27 * This job requests Wikifunctions to re-render the fragment via the
28 * wikilambda_function_call Action API and updates the cached value
29 *
30 * Queued by:
31 * * AWFragmentStore::getLatestRenderedAWFragment when fresh fragment
32 *   is missing. This can be called from:
33 *   * ActionApi/ApiAbstractWikiRunFragment
34 *   * maintenance/updateAbstractWikiArticleStore
35 */
36class CacheAbstractContentFragmentJob extends Job implements GenericParameterJob {
37
38    private Config $config;
39    private LoggerInterface $logger;
40    private AbstractWikiRequest $abstractWikiRequest;
41
42    /**
43     * @inheritDoc
44     */
45    public function __construct( array $params ) {
46        parent::__construct( 'cacheAbstractContentFragment', $params );
47
48        // Non-injected items
49        $this->config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'WikiLambda' );
50        $this->logger = LoggerFactory::getInstance( 'WikiLambdaAbstract' );
51
52        $this->abstractWikiRequest = WikiLambdaServices::getAbstractWikiRequest();
53
54        // TODO (T434284) Remove date from the log once the old jobs have been drained
55        $this->logger->info(
56            __CLASS__ . ' created',
57            [
58                'qid' => $params['qid'],
59                'language' => $params['language'],
60                'date' => $params['date'] ?? null,
61                'datetime' => $params['datetime'] ?? null,
62                'fragmentKey' => $params['fragmentKey']
63            ]
64        );
65    }
66
67    /**
68     * Asynchronous job to re-generate and rfresh the rendered fragment in the cache.
69     * * Makes a remote call to Wikifunctions wikilambda_function_call to evaluate a fragment fragment
70     * * Sanitizes the HTML response.
71     * * Caches resulting fragment under fresh and stale cache keys.
72     *
73     * @return bool
74     */
75    public function run() {
76        $fragment = $this->params['fragment'];
77        $qid = $this->params['qid'];
78        $language = $this->params['language'];
79        $fragmentKey = $this->params['fragmentKey'];
80
81        // TODO (T434284) Transitional code: we should remove this when the
82        // old job queue drains, and we are ready to use datetime directly:
83        if ( isset( $this->params['datetime'] ) ) {
84            $datetime = $this->params['datetime'];
85        } else {
86            // Transform date 'Y-m-d' into compatible datetime if the
87            // job queue has any lingering job with the old parameters
88            $datetime = ( new ConvertibleTimestamp( $this->params['date'] . ' 00:00:00' ) )
89                ->getTimestamp( TS::MW );
90        }
91
92        $this->logger->info(
93            __CLASS__ . ' initiated for qid:{qid} language:{language} and datetime:{datetime} ',
94            [
95                'qid' => $qid,
96                'language' => $language,
97                'datetime' => $datetime,
98                'fragmentKey' => $fragmentKey
99            ]
100        );
101
102        $cachedValue = $this->abstractWikiRequest->fetchRenderedAWFragment(
103            $fragment,
104            $qid,
105            $language,
106            $datetime,
107            $fragmentKey
108        );
109
110        $httpStatusCode = $cachedValue['success'] ? HttpStatus::OK : (int)$cachedValue['value']['httpStatusCode'];
111
112        // Find out if the failure was due to rendering service unavailable, and retry only in that case
113        if ( $cachedValue[ 'success' ] === false ) {
114            if (
115                $httpStatusCode === HttpStatus::TOO_MANY_REQUESTS ||
116                $httpStatusCode === HttpStatus::SERVICE_UNAVAILABLE
117            ) {
118                $this->logger->warning( __CLASS__
119                    . ' rate limited ({httpStatusCode}) for qid:{qid} language:{language} and datetime:{datetime} ',
120                    [
121                        'qid' => $qid,
122                        'language' => $language,
123                        'datetime' => $datetime,
124                        'fragmentKey' => $fragmentKey,
125                        'httpStatusCode' => $httpStatusCode
126                    ]
127                );
128                // Return false to force retry job in this case
129                return false;
130            }
131        }
132
133        $this->logger->info( __CLASS__
134            . ' refresh fragment status:{httpStatusCode} for qid:{qid} language:{language} and datetime:{datetime} ',
135            [
136                'qid' => $qid,
137                'language' => $language,
138                'datetime' => $datetime,
139                'fragmentKey' => $fragmentKey,
140                'httpStatusCode' => $httpStatusCode
141            ]
142        );
143        // Return true to avoid retries
144        return true;
145    }
146
147    /**
148     * @inheritDoc
149     */
150    public function ignoreDuplicates() {
151        // We've carefully chosen the parameters so this Job is shared across multiple uses, so don't run it
152        // in parallel and have MediaWiki de-duplicate requests.
153        return true;
154    }
155
156    /**
157     * @inheritDoc
158     */
159    public function getDeduplicationInfo() {
160        $info = parent::getDeduplicationInfo();
161        // When deduplicating, only keep fragment-defining parameters (qid, language and fragment)
162        $info[ 'params' ] = [
163            'qid' => $this->params['qid'],
164            'language' => $this->params['language'],
165            'fragment' => $this->params['fragment']
166        ];
167
168        return $info;
169    }
170
171    /**
172     * @inheritDoc
173     */
174    public function allowRetries() {
175        return true;
176    }
177}