Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.71% |
174 / 203 |
|
75.00% |
12 / 16 |
CRAP | |
0.00% |
0 / 1 |
PageEditStash | |
85.71% |
174 / 203 |
|
75.00% |
12 / 16 |
58.58 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
parseAndCache | |
74.14% |
43 / 58 |
|
0.00% |
0 / 1 |
10.40 | |||
checkCache | |
83.33% |
50 / 60 |
|
0.00% |
0 / 1 |
16.04 | |||
incrStatsByContent | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getAndWaitForStashValue | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
fetchInputText | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
stashInputText | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
lastEditTime | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getContentHash | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getStashKey | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getStashValue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
storeStashValue | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
6.04 | |||
pruneExcessStashedEntries | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
recentStashEntryCount | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
serializeStashInfo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unserializeStashInfo | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Storage; |
22 | |
23 | use BagOStuff; |
24 | use Content; |
25 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
26 | use MediaWiki\HookContainer\HookContainer; |
27 | use MediaWiki\HookContainer\HookRunner; |
28 | use MediaWiki\Page\PageIdentity; |
29 | use MediaWiki\Page\WikiPageFactory; |
30 | use MediaWiki\Parser\ParserOutput; |
31 | use MediaWiki\Parser\ParserOutputFlags; |
32 | use MediaWiki\Revision\SlotRecord; |
33 | use MediaWiki\Storage\Hook\ParserOutputStashForEditHook; |
34 | use MediaWiki\User\UserEditTracker; |
35 | use MediaWiki\User\UserFactory; |
36 | use MediaWiki\User\UserIdentity; |
37 | use Psr\Log\LoggerInterface; |
38 | use stdClass; |
39 | use Wikimedia\Rdbms\IConnectionProvider; |
40 | use Wikimedia\ScopedCallback; |
41 | use WikiPage; |
42 | |
43 | /** |
44 | * Manage the pre-emptive page parsing for edits to wiki pages. |
45 | * |
46 | * This is written to by ApiStashEdit, and consumed by ApiEditPage |
47 | * and EditPage (via PageUpdaterFactory and DerivedPageDataUpdater). |
48 | * |
49 | * See also mediawiki.action.edit/stash.js. |
50 | * |
51 | * @since 1.34 |
52 | * @ingroup Page |
53 | */ |
54 | class PageEditStash { |
55 | /** @var BagOStuff */ |
56 | private $cache; |
57 | /** @var IConnectionProvider */ |
58 | private $dbProvider; |
59 | /** @var LoggerInterface */ |
60 | private $logger; |
61 | /** @var StatsdDataFactoryInterface */ |
62 | private $stats; |
63 | /** @var ParserOutputStashForEditHook */ |
64 | private $hookRunner; |
65 | /** @var UserEditTracker */ |
66 | private $userEditTracker; |
67 | /** @var UserFactory */ |
68 | private $userFactory; |
69 | /** @var WikiPageFactory */ |
70 | private $wikiPageFactory; |
71 | /** @var int */ |
72 | private $initiator; |
73 | |
74 | public const ERROR_NONE = 'stashed'; |
75 | public const ERROR_PARSE = 'error_parse'; |
76 | public const ERROR_CACHE = 'error_cache'; |
77 | public const ERROR_UNCACHEABLE = 'uncacheable'; |
78 | public const ERROR_BUSY = 'busy'; |
79 | |
80 | public const PRESUME_FRESH_TTL_SEC = 30; |
81 | public const MAX_CACHE_TTL = 300; // 5 minutes |
82 | public const MAX_SIGNATURE_TTL = 60; |
83 | |
84 | private const MAX_CACHE_RECENT = 2; |
85 | |
86 | public const INITIATOR_USER = 1; |
87 | public const INITIATOR_JOB_OR_CLI = 2; |
88 | |
89 | /** |
90 | * @param BagOStuff $cache |
91 | * @param IConnectionProvider $dbProvider |
92 | * @param LoggerInterface $logger |
93 | * @param StatsdDataFactoryInterface $stats |
94 | * @param UserEditTracker $userEditTracker |
95 | * @param UserFactory $userFactory |
96 | * @param WikiPageFactory $wikiPageFactory |
97 | * @param HookContainer $hookContainer |
98 | * @param int $initiator Class INITIATOR__* constant |
99 | */ |
100 | public function __construct( |
101 | BagOStuff $cache, |
102 | IConnectionProvider $dbProvider, |
103 | LoggerInterface $logger, |
104 | StatsdDataFactoryInterface $stats, |
105 | UserEditTracker $userEditTracker, |
106 | UserFactory $userFactory, |
107 | WikiPageFactory $wikiPageFactory, |
108 | HookContainer $hookContainer, |
109 | $initiator |
110 | ) { |
111 | $this->cache = $cache; |
112 | $this->dbProvider = $dbProvider; |
113 | $this->logger = $logger; |
114 | $this->stats = $stats; |
115 | $this->userEditTracker = $userEditTracker; |
116 | $this->userFactory = $userFactory; |
117 | $this->wikiPageFactory = $wikiPageFactory; |
118 | $this->hookRunner = new HookRunner( $hookContainer ); |
119 | $this->initiator = $initiator; |
120 | } |
121 | |
122 | /** |
123 | * @param PageUpdater $pageUpdater (a WikiPage instance is also supported but deprecated) |
124 | * @param Content $content Edit content |
125 | * @param UserIdentity $user |
126 | * @param string $summary Edit summary |
127 | * @return string Class ERROR_* constant |
128 | */ |
129 | public function parseAndCache( $pageUpdater, Content $content, UserIdentity $user, string $summary ) { |
130 | $logger = $this->logger; |
131 | |
132 | if ( $pageUpdater instanceof WikiPage ) { |
133 | wfDeprecated( __METHOD__ . ' with WikiPage instance', '1.42' ); |
134 | $pageUpdater = $pageUpdater->newPageUpdater( $user ); |
135 | } |
136 | |
137 | $page = $pageUpdater->getPage(); |
138 | $key = $this->getStashKey( $page, $this->getContentHash( $content ), $user ); |
139 | $fname = __METHOD__; |
140 | |
141 | // Use the primary DB to allow for fast blocking locks on the "save path" where this |
142 | // value might actually be used to complete a page edit. If the edit submission request |
143 | // happens before this edit stash requests finishes, then the submission will block until |
144 | // the stash request finishes parsing. For the lock acquisition below, there is not much |
145 | // need to duplicate parsing of the same content/user/summary bundle, so try to avoid |
146 | // blocking at all here. |
147 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
148 | if ( !$dbw->lock( $key, $fname, 0 ) ) { |
149 | // De-duplicate requests on the same key |
150 | return self::ERROR_BUSY; |
151 | } |
152 | /** @noinspection PhpUnusedLocalVariableInspection */ |
153 | $unlocker = new ScopedCallback( static function () use ( $dbw, $key, $fname ) { |
154 | $dbw->unlock( $key, $fname ); |
155 | } ); |
156 | |
157 | $cutoffTime = time() - self::PRESUME_FRESH_TTL_SEC; |
158 | |
159 | // Reuse any freshly build matching edit stash cache |
160 | $editInfo = $this->getStashValue( $key ); |
161 | if ( $editInfo && (int)wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) { |
162 | $alreadyCached = true; |
163 | } else { |
164 | $pageUpdater->setContent( SlotRecord::MAIN, $content ); |
165 | |
166 | $update = $pageUpdater->prepareUpdate( EDIT_INTERNAL ); // applies pre-safe transform |
167 | $output = $update->getCanonicalParserOutput(); // causes content to be parsed |
168 | $output->setCacheTime( $update->getRevision()->getTimestamp() ); |
169 | |
170 | // emulate a cache value that kind of looks like a PreparedEdit, for use below |
171 | $editInfo = (object)[ |
172 | 'pstContent' => $update->getRawContent( SlotRecord::MAIN ), |
173 | 'output' => $output, |
174 | 'timestamp' => $output->getCacheTime() |
175 | ]; |
176 | |
177 | $alreadyCached = false; |
178 | } |
179 | |
180 | $logContext = [ 'cachekey' => $key, 'title' => (string)$page ]; |
181 | |
182 | if ( $editInfo->output ) { |
183 | // Let extensions add ParserOutput metadata or warm other caches |
184 | $legacyUser = $this->userFactory->newFromUserIdentity( $user ); |
185 | $legacyPage = $this->wikiPageFactory->newFromTitle( $page ); |
186 | $this->hookRunner->onParserOutputStashForEdit( |
187 | $legacyPage, $content, $editInfo->output, $summary, $legacyUser ); |
188 | |
189 | if ( $alreadyCached ) { |
190 | $logger->debug( "Parser output for key '{cachekey}' already cached.", $logContext ); |
191 | |
192 | return self::ERROR_NONE; |
193 | } |
194 | |
195 | $code = $this->storeStashValue( |
196 | $key, |
197 | $editInfo->pstContent, |
198 | $editInfo->output, |
199 | $editInfo->timestamp, |
200 | $user |
201 | ); |
202 | |
203 | if ( $code === true ) { |
204 | $logger->debug( "Cached parser output for key '{cachekey}'.", $logContext ); |
205 | |
206 | return self::ERROR_NONE; |
207 | } elseif ( $code === 'uncacheable' ) { |
208 | $logger->info( |
209 | "Uncacheable parser output for key '{cachekey}' [{code}].", |
210 | $logContext + [ 'code' => $code ] |
211 | ); |
212 | |
213 | return self::ERROR_UNCACHEABLE; |
214 | } else { |
215 | $logger->error( |
216 | "Failed to cache parser output for key '{cachekey}'.", |
217 | $logContext + [ 'code' => $code ] |
218 | ); |
219 | |
220 | return self::ERROR_CACHE; |
221 | } |
222 | } |
223 | |
224 | return self::ERROR_PARSE; |
225 | } |
226 | |
227 | /** |
228 | * Check that a prepared edit is in cache and still up-to-date |
229 | * |
230 | * This method blocks if the prepared edit is already being rendered, |
231 | * waiting until rendering finishes before doing final validity checks. |
232 | * |
233 | * The cache is rejected if template or file changes are detected. |
234 | * Note that foreign template or file transclusions are not checked. |
235 | * |
236 | * This returns an object with the following fields: |
237 | * - pstContent: the Content after pre-save-transform |
238 | * - output: the ParserOutput instance |
239 | * - timestamp: the timestamp of the parse |
240 | * - edits: author edit count if they are logged in or NULL otherwise |
241 | * |
242 | * @param PageIdentity $page |
243 | * @param Content $content |
244 | * @param UserIdentity $user to get parser options from |
245 | * @return stdClass|false Returns edit stash object or false on cache miss |
246 | */ |
247 | public function checkCache( PageIdentity $page, Content $content, UserIdentity $user ) { |
248 | $legacyUser = $this->userFactory->newFromUserIdentity( $user ); |
249 | if ( |
250 | // The context is not an HTTP POST request |
251 | !$legacyUser->getRequest()->wasPosted() || |
252 | // The context is a CLI script or a job runner HTTP POST request |
253 | $this->initiator !== self::INITIATOR_USER || |
254 | // The editor account is a known bot |
255 | $legacyUser->isBot() |
256 | ) { |
257 | // Avoid wasted queries and statsd pollution |
258 | return false; |
259 | } |
260 | |
261 | $logger = $this->logger; |
262 | |
263 | $key = $this->getStashKey( $page, $this->getContentHash( $content ), $user ); |
264 | $logContext = [ |
265 | 'key' => $key, |
266 | 'title' => (string)$page, |
267 | 'user' => $user->getName() |
268 | ]; |
269 | |
270 | $editInfo = $this->getAndWaitForStashValue( $key ); |
271 | if ( !is_object( $editInfo ) || !$editInfo->output ) { |
272 | $this->incrStatsByContent( 'cache_misses.no_stash', $content ); |
273 | if ( $this->recentStashEntryCount( $user ) > 0 ) { |
274 | $logger->info( "Empty cache for key '{key}' but not for user.", $logContext ); |
275 | } else { |
276 | $logger->debug( "Empty cache for key '{key}'.", $logContext ); |
277 | } |
278 | |
279 | return false; |
280 | } |
281 | |
282 | $age = time() - (int)wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() ); |
283 | $logContext['age'] = $age; |
284 | |
285 | $isCacheUsable = true; |
286 | if ( $age <= self::PRESUME_FRESH_TTL_SEC ) { |
287 | // Assume nothing changed in this time |
288 | $this->incrStatsByContent( 'cache_hits.presumed_fresh', $content ); |
289 | $logger->debug( "Timestamp-based cache hit for key '{key}'.", $logContext ); |
290 | } elseif ( !$user->isRegistered() ) { |
291 | $lastEdit = $this->lastEditTime( $user ); |
292 | $cacheTime = $editInfo->output->getCacheTime(); |
293 | if ( $lastEdit < $cacheTime ) { |
294 | // Logged-out user made no local upload/template edits in the meantime |
295 | $this->incrStatsByContent( 'cache_hits.presumed_fresh', $content ); |
296 | $logger->debug( "Edit check based cache hit for key '{key}'.", $logContext ); |
297 | } else { |
298 | $isCacheUsable = false; |
299 | $this->incrStatsByContent( 'cache_misses.proven_stale', $content ); |
300 | $logger->info( "Stale cache for key '{key}' due to outside edits.", $logContext ); |
301 | } |
302 | } else { |
303 | if ( $editInfo->edits === $this->userEditTracker->getUserEditCount( $user ) ) { |
304 | // Logged-in user made no local upload/template edits in the meantime |
305 | $this->incrStatsByContent( 'cache_hits.presumed_fresh', $content ); |
306 | $logger->debug( "Edit count based cache hit for key '{key}'.", $logContext ); |
307 | } else { |
308 | $isCacheUsable = false; |
309 | $this->incrStatsByContent( 'cache_misses.proven_stale', $content ); |
310 | $logger->info( "Stale cache for key '{key}'due to outside edits.", $logContext ); |
311 | } |
312 | } |
313 | |
314 | if ( !$isCacheUsable ) { |
315 | return false; |
316 | } |
317 | |
318 | if ( $editInfo->output->getOutputFlag( ParserOutputFlags::VARY_REVISION ) ) { |
319 | // This can be used for the initial parse, e.g. for filters or doUserEditContent(), |
320 | // but a second parse will be triggered in doEditUpdates() no matter what |
321 | $logger->info( |
322 | "Cache for key '{key}' has vary-revision; post-insertion parse inevitable.", |
323 | $logContext |
324 | ); |
325 | } else { |
326 | static $flagsMaybeReparse = [ |
327 | // Similar to the above if we didn't guess the ID correctly |
328 | ParserOutputFlags::VARY_REVISION_ID, |
329 | // Similar to the above if we didn't guess the timestamp correctly |
330 | ParserOutputFlags::VARY_REVISION_TIMESTAMP, |
331 | // Similar to the above if we didn't guess the content correctly |
332 | ParserOutputFlags::VARY_REVISION_SHA1, |
333 | // Similar to the above if we didn't guess page ID correctly |
334 | ParserOutputFlags::VARY_PAGE_ID, |
335 | ]; |
336 | foreach ( $flagsMaybeReparse as $flag ) { |
337 | if ( $editInfo->output->getOutputFlag( $flag ) ) { |
338 | $logger->debug( |
339 | "Cache for key '{key}' has $flag; post-insertion parse possible.", |
340 | $logContext |
341 | ); |
342 | } |
343 | } |
344 | } |
345 | |
346 | return $editInfo; |
347 | } |
348 | |
349 | /** |
350 | * @param string $subkey |
351 | * @param Content $content |
352 | */ |
353 | private function incrStatsByContent( $subkey, Content $content ) { |
354 | $this->stats->increment( 'editstash.' . $subkey ); // overall for b/c |
355 | $this->stats->increment( 'editstash_by_model.' . $content->getModel() . '.' . $subkey ); |
356 | } |
357 | |
358 | /** |
359 | * @param string $key |
360 | * @return bool|stdClass |
361 | */ |
362 | private function getAndWaitForStashValue( $key ) { |
363 | $editInfo = $this->getStashValue( $key ); |
364 | |
365 | if ( !$editInfo ) { |
366 | $start = microtime( true ); |
367 | // We ignore user aborts and keep parsing. Block on any prior parsing |
368 | // so as to use its results and make use of the time spent parsing. |
369 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
370 | if ( $dbw->lock( $key, __METHOD__, 30 ) ) { |
371 | $editInfo = $this->getStashValue( $key ); |
372 | $dbw->unlock( $key, __METHOD__ ); |
373 | } |
374 | |
375 | $timeMs = 1000 * max( 0, microtime( true ) - $start ); |
376 | $this->stats->timing( 'editstash.lock_wait_time', $timeMs ); |
377 | } |
378 | |
379 | return $editInfo; |
380 | } |
381 | |
382 | /** |
383 | * @param string $textHash |
384 | * @return string|bool Text or false if missing |
385 | */ |
386 | public function fetchInputText( $textHash ) { |
387 | $textKey = $this->cache->makeKey( 'stashedit', 'text', $textHash ); |
388 | |
389 | return $this->cache->get( $textKey ); |
390 | } |
391 | |
392 | /** |
393 | * @param string $text |
394 | * @param string $textHash |
395 | * @return bool Success |
396 | */ |
397 | public function stashInputText( $text, $textHash ) { |
398 | $textKey = $this->cache->makeKey( 'stashedit', 'text', $textHash ); |
399 | |
400 | return $this->cache->set( |
401 | $textKey, |
402 | $text, |
403 | self::MAX_CACHE_TTL, |
404 | BagOStuff::WRITE_ALLOW_SEGMENTS |
405 | ); |
406 | } |
407 | |
408 | /** |
409 | * @param UserIdentity $user |
410 | * @return string|null TS_MW timestamp or null |
411 | */ |
412 | private function lastEditTime( UserIdentity $user ) { |
413 | $time = $this->dbProvider->getReplicaDatabase()->newSelectQueryBuilder() |
414 | ->select( 'MAX(rc_timestamp)' ) |
415 | ->from( 'recentchanges' ) |
416 | ->join( 'actor', null, 'actor_id=rc_actor' ) |
417 | ->where( [ 'actor_name' => $user->getName() ] ) |
418 | ->caller( __METHOD__ ) |
419 | ->fetchField(); |
420 | |
421 | return wfTimestampOrNull( TS_MW, $time ); |
422 | } |
423 | |
424 | /** |
425 | * Get hash of the content, factoring in model/format |
426 | * |
427 | * @param Content $content |
428 | * @return string |
429 | */ |
430 | private function getContentHash( Content $content ) { |
431 | return sha1( implode( "\n", [ |
432 | $content->getModel(), |
433 | $content->getDefaultFormat(), |
434 | $content->serialize( $content->getDefaultFormat() ) |
435 | ] ) ); |
436 | } |
437 | |
438 | /** |
439 | * Get the temporary prepared edit stash key for a user |
440 | * |
441 | * This key can be used for caching prepared edits provided: |
442 | * - a) The $user was used for PST options |
443 | * - b) The parser output was made from the PST using cannonical matching options |
444 | * |
445 | * @param PageIdentity $page |
446 | * @param string $contentHash Result of getContentHash() |
447 | * @param UserIdentity $user User to get parser options from |
448 | * @return string |
449 | */ |
450 | private function getStashKey( PageIdentity $page, $contentHash, UserIdentity $user ) { |
451 | return $this->cache->makeKey( |
452 | 'stashedit-info-v2', |
453 | md5( "{$page->getNamespace()}\n{$page->getDBkey()}" ), |
454 | // Account for the edit model/text |
455 | $contentHash, |
456 | // Account for user name related variables like signatures |
457 | md5( "{$user->getId()}\n{$user->getName()}" ) |
458 | ); |
459 | } |
460 | |
461 | /** |
462 | * @param string $key |
463 | * @return stdClass|bool Object map (pstContent,output,outputID,timestamp,edits) or false |
464 | */ |
465 | private function getStashValue( $key ) { |
466 | $serial = $this->cache->get( $key ); |
467 | |
468 | return $this->unserializeStashInfo( $serial ); |
469 | } |
470 | |
471 | /** |
472 | * Build a value to store in memcached based on the PST content and parser output |
473 | * |
474 | * This makes a simple version of WikiPage::prepareContentForEdit() as stash info |
475 | * |
476 | * @param string $key |
477 | * @param Content $pstContent Pre-Save transformed content |
478 | * @param ParserOutput $parserOutput |
479 | * @param string $timestamp TS_MW |
480 | * @param UserIdentity $user |
481 | * @return string|bool True or an error code |
482 | */ |
483 | private function storeStashValue( |
484 | $key, |
485 | Content $pstContent, |
486 | ParserOutput $parserOutput, |
487 | $timestamp, |
488 | UserIdentity $user |
489 | ) { |
490 | // If an item is renewed, mind the cache TTL determined by config and parser functions. |
491 | // Put an upper limit on the TTL to avoid extreme template/file staleness. |
492 | $age = time() - (int)wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() ); |
493 | $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL ); |
494 | // Avoid extremely stale user signature timestamps (T84843) |
495 | if ( $parserOutput->getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) { |
496 | $ttl = min( $ttl, self::MAX_SIGNATURE_TTL ); |
497 | } |
498 | |
499 | if ( $ttl <= 0 ) { |
500 | return 'uncacheable'; // low TTL due to a tag, magic word, or signature? |
501 | } |
502 | |
503 | // Store what is actually needed and split the output into another key (T204742) |
504 | $stashInfo = (object)[ |
505 | 'pstContent' => $pstContent, |
506 | 'output' => $parserOutput, |
507 | 'timestamp' => $timestamp, |
508 | 'edits' => $this->userEditTracker->getUserEditCount( $user ), |
509 | ]; |
510 | $serial = $this->serializeStashInfo( $stashInfo ); |
511 | if ( $serial === false ) { |
512 | return 'store_error'; |
513 | } |
514 | |
515 | $ok = $this->cache->set( $key, $serial, $ttl, BagOStuff::WRITE_ALLOW_SEGMENTS ); |
516 | if ( $ok ) { |
517 | // These blobs can waste slots in low cardinality memcached slabs |
518 | $this->pruneExcessStashedEntries( $user, $key ); |
519 | } |
520 | |
521 | return $ok ? true : 'store_error'; |
522 | } |
523 | |
524 | /** |
525 | * @param UserIdentity $user |
526 | * @param string $newKey |
527 | */ |
528 | private function pruneExcessStashedEntries( UserIdentity $user, $newKey ) { |
529 | $key = $this->cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) ); |
530 | |
531 | $keyList = $this->cache->get( $key ) ?: []; |
532 | if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) { |
533 | $oldestKey = array_shift( $keyList ); |
534 | $this->cache->delete( $oldestKey, BagOStuff::WRITE_PRUNE_SEGMENTS ); |
535 | } |
536 | |
537 | $keyList[] = $newKey; |
538 | $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL ); |
539 | } |
540 | |
541 | /** |
542 | * @param UserIdentity $user |
543 | * @return int |
544 | */ |
545 | private function recentStashEntryCount( UserIdentity $user ) { |
546 | $key = $this->cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) ); |
547 | |
548 | return count( $this->cache->get( $key ) ?: [] ); |
549 | } |
550 | |
551 | private function serializeStashInfo( stdClass $stashInfo ) { |
552 | // @todo: use JSON with ParserOutput and Content |
553 | return serialize( $stashInfo ); |
554 | } |
555 | |
556 | private function unserializeStashInfo( $serial ) { |
557 | if ( is_string( $serial ) ) { |
558 | // @todo: use JSON with ParserOutput and Content |
559 | $stashInfo = unserialize( $serial ); |
560 | if ( is_object( $stashInfo ) && $stashInfo->output instanceof ParserOutput ) { |
561 | return $stashInfo; |
562 | } |
563 | } |
564 | |
565 | return false; |
566 | } |
567 | } |