Don't check namespace in SpecialWantedtemplates
[lhc/web/wiklou.git] / includes / jobqueue / jobs / RefreshLinksJob.php
1 <?php
2 /**
3 * Job to update link tables for pages
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 JobQueue
22 */
23
24 /**
25 * Job to update link tables for pages
26 *
27 * This job comes in a few variants:
28 * - a) Recursive jobs to update links for backlink pages for a given title.
29 * These jobs have (recursive:true,table:<table>) set.
30 * - b) Jobs to update links for a set of pages (the job title is ignored).
31 * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set.
32 * - c) Jobs to update links for a single page (the job title)
33 * These jobs need no extra fields set.
34 *
35 * @ingroup JobQueue
36 */
37 class RefreshLinksJob extends Job {
38 const PARSE_THRESHOLD_SEC = 1.0;
39
40 const CLOCK_FUDGE = 10;
41
42 function __construct( Title $title, array $params ) {
43 parent::__construct( 'refreshLinks', $title, $params );
44 // A separate type is used just for cascade-protected backlinks
45 if ( !empty( $this->params['prioritize'] ) ) {
46 $this->command .= 'Prioritized';
47 }
48 // Base backlink update jobs and per-title update jobs can be de-duplicated.
49 // If template A changes twice before any jobs run, a clean queue will have:
50 // (A base, A base)
51 // The second job is ignored by the queue on insertion.
52 // Suppose, many pages use template A, and that template itself uses template B.
53 // An edit to both will first create two base jobs. A clean FIFO queue will have:
54 // (A base, B base)
55 // When these jobs run, the queue will have per-title and remnant partition jobs:
56 // (titleX,titleY,titleZ,...,A remnant,titleM,titleN,titleO,...,B remnant)
57 // Some these jobs will be the same, and will automatically be ignored by
58 // the queue upon insertion. Some title jobs will run before the duplicate is
59 // inserted, so the work will still be done twice in those cases. More titles
60 // can be de-duplicated as the remnant jobs continue to be broken down. This
61 // works best when $wgUpdateRowsPerJob, and either the pages have few backlinks
62 // and/or the backlink sets for pages A and B are almost identical.
63 $this->removeDuplicates = !isset( $params['range'] )
64 && ( !isset( $params['pages'] ) || count( $params['pages'] ) == 1 );
65 }
66
67 function run() {
68 global $wgUpdateRowsPerJob;
69
70 // Job to update all (or a range of) backlink pages for a page
71 if ( !empty( $this->params['recursive'] ) ) {
72 // Carry over information for de-duplication
73 $extraParams = $this->getRootJobParams();
74 // Avoid slave lag when fetching templates.
75 // When the outermost job is run, we know that the caller that enqueued it must have
76 // committed the relevant changes to the DB by now. At that point, record the master
77 // position and pass it along as the job recursively breaks into smaller range jobs.
78 // Hopefully, when leaf jobs are popped, the slaves will have reached that position.
79 if ( isset( $this->params['masterPos'] ) ) {
80 $extraParams['masterPos'] = $this->params['masterPos'];
81 } elseif ( wfGetLB()->getServerCount() > 1 ) {
82 $extraParams['masterPos'] = wfGetLB()->getMasterPos();
83 } else {
84 $extraParams['masterPos'] = false;
85 }
86 // Convert this into no more than $wgUpdateRowsPerJob RefreshLinks per-title
87 // jobs and possibly a recursive RefreshLinks job for the rest of the backlinks
88 $jobs = BacklinkJobUtils::partitionBacklinkJob(
89 $this,
90 $wgUpdateRowsPerJob,
91 1, // job-per-title
92 array( 'params' => $extraParams )
93 );
94 JobQueueGroup::singleton()->push( $jobs );
95 // Job to update link tables for a set of titles
96 } elseif ( isset( $this->params['pages'] ) ) {
97 foreach ( $this->params['pages'] as $pageId => $nsAndKey ) {
98 list( $ns, $dbKey ) = $nsAndKey;
99 $this->runForTitle( Title::makeTitleSafe( $ns, $dbKey ) );
100 }
101 // Job to update link tables for a given title
102 } else {
103 $this->runForTitle( $this->title );
104 }
105
106 return true;
107 }
108
109 /**
110 * @param Title $title
111 * @return bool
112 */
113 protected function runForTitle( Title $title = null ) {
114 $linkCache = LinkCache::singleton();
115 $linkCache->clear();
116
117 if ( is_null( $title ) ) {
118 $this->setLastError( "refreshLinks: Invalid title" );
119 return false;
120 }
121
122 // Wait for the DB of the current/next slave DB handle to catch up to the master.
123 // This way, we get the correct page_latest for templates or files that just changed
124 // milliseconds ago, having triggered this job to begin with.
125 if ( isset( $this->params['masterPos'] ) && $this->params['masterPos'] !== false ) {
126 wfGetLB()->waitFor( $this->params['masterPos'] );
127 }
128
129 $page = WikiPage::factory( $title );
130
131 // Fetch the current revision...
132 $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL );
133 if ( !$revision ) {
134 $this->setLastError( "refreshLinks: Article not found {$title->getPrefixedDBkey()}" );
135 return false; // XXX: what if it was just deleted?
136 }
137 $content = $revision->getContent( Revision::RAW );
138 if ( !$content ) {
139 // If there is no content, pretend the content is empty
140 $content = $revision->getContentHandler()->makeEmptyContent();
141 }
142
143 $parserOutput = false;
144 $parserOptions = $page->makeParserOptions( 'canonical' );
145 // If page_touched changed after this root job, then it is likely that
146 // any views of the pages already resulted in re-parses which are now in
147 // cache. The cache can be reused to avoid expensive parsing in some cases.
148 if ( isset( $this->params['rootJobTimestamp'] ) ) {
149 $opportunistic = !empty( $this->params['isOpportunistic'] );
150
151 $skewedTimestamp = $this->params['rootJobTimestamp'];
152 if ( $opportunistic ) {
153 // Neither clock skew nor DB snapshot/slave lag matter much for such
154 // updates; focus on reusing the (often recently updated) cache
155 } else {
156 // For transclusion updates, the template changes must be reflected
157 $skewedTimestamp = wfTimestamp( TS_MW,
158 wfTimestamp( TS_UNIX, $skewedTimestamp ) + self::CLOCK_FUDGE
159 );
160 }
161
162 if ( $page->getLinksTimestamp() > $skewedTimestamp ) {
163 // Something already updated the backlinks since this job was made
164 return true;
165 }
166
167 if ( $page->getTouched() >= $skewedTimestamp || $opportunistic ) {
168 // Something bumped page_touched since this job was made
169 // or the cache is otherwise suspected to be up-to-date
170 $parserOutput = ParserCache::singleton()->getDirty( $page, $parserOptions );
171 if ( $parserOutput && $parserOutput->getCacheTime() < $skewedTimestamp ) {
172 $parserOutput = false; // too stale
173 }
174 }
175 }
176
177 // Fetch the current revision and parse it if necessary...
178 if ( $parserOutput == false ) {
179 $start = microtime( true );
180 // Revision ID must be passed to the parser output to get revision variables correct
181 $parserOutput = $content->getParserOutput(
182 $title, $revision->getId(), $parserOptions, false );
183 $ellapsed = microtime( true ) - $start;
184 // If it took a long time to render, then save this back to the cache to avoid
185 // wasted CPU by other apaches or job runners. We don't want to always save to
186 // cache as this can cause high cache I/O and LRU churn when a template changes.
187 if ( $ellapsed >= self::PARSE_THRESHOLD_SEC
188 && $page->isParserCacheUsed( $parserOptions, $revision->getId() )
189 && $parserOutput->isCacheable()
190 ) {
191 $ctime = wfTimestamp( TS_MW, (int)$start ); // cache time
192 ParserCache::singleton()->save(
193 $parserOutput, $page, $parserOptions, $ctime, $revision->getId()
194 );
195 }
196 }
197
198 $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput );
199 DataUpdate::runUpdates( $updates );
200
201 InfoAction::invalidateCache( $title );
202
203 return true;
204 }
205
206 public function getDeduplicationInfo() {
207 $info = parent::getDeduplicationInfo();
208 if ( is_array( $info['params'] ) ) {
209 // Don't let highly unique "masterPos" values ruin duplicate detection
210 unset( $info['params']['masterPos'] );
211 // For per-pages jobs, the job title is that of the template that changed
212 // (or similar), so remove that since it ruins duplicate detection
213 if ( isset( $info['pages'] ) ) {
214 unset( $info['namespace'] );
215 unset( $info['title'] );
216 }
217 }
218
219 return $info;
220 }
221
222 public function workItemCount() {
223 return isset( $this->params['pages'] ) ? count( $this->params['pages'] ) : 1;
224 }
225 }