1<?php
2
3use MediaWiki\Linker\LinkTarget;
4use MediaWiki\MediaWikiServices;
5use Psr\Log\LoggerInterface;
6use Wikimedia\Rdbms\IDatabase;
7
8/**
9 * Class for fixing stale WANObjectCache keys using a purge event source
10 *
11 * This is useful for expiring keys that missed fire-and-forget purges. This uses the
12 * recentchanges table as a reliable stream to make certain keys reach consistency
13 * as soon as the underlying replica database catches up. These means that critical
14 * keys will not escape getting purged simply due to brief hiccups in the network,
15 * which are more prone to happen across datacenters.
16 *
17 * ----
18 * "I was trying to cheat death. I was only trying to surmount for a little while the
19 * darkness that all my life I surely knew was going to come rolling in on me some day
20 * and obliterate me. I was only to stay alive a little brief while longer, after I was
21 * already gone. To stay in the light, to be with the living, a little while past my time."
22 *   -- Notes for "Blues of a Lifetime", by [[Cornell Woolrich]]
23 *
24 * @since 1.28
25 */
26class WANCacheReapUpdate implements DeferrableUpdate {
27	/** @var IDatabase */
28	private $db;
29	/** @var LoggerInterface */
30	private $logger;
31
32	/**
33	 * @param IDatabase $db
34	 * @param LoggerInterface $logger
35	 */
36	public function __construct( IDatabase $db, LoggerInterface $logger ) {
37		$this->db = $db;
38		$this->logger = $logger;
39	}
40
41	public function doUpdate() {
42		$reaper = new WANObjectCacheReaper(
43			MediaWikiServices::getInstance()->getMainWANObjectCache(),
44			ObjectCache::getLocalClusterInstance(),
45			[ $this, 'getTitleChangeEvents' ],
46			[ $this, 'getEventAffectedKeys' ],
47			[
48				'channel' => 'table:recentchanges:' . $this->db->getDomainID(),
49				'logger' => $this->logger
50			]
51		);
52
53		$reaper->invoke( 100 );
54	}
55
56	/**
57	 * @see WANObjectCacheRepear
58	 *
59	 * @param int $start
60	 * @param int $id
61	 * @param int $end
62	 * @param int $limit
63	 * @return TitleValue[]
64	 */
65	public function getTitleChangeEvents( $start, $id, $end, $limit ) {
66		$db = $this->db;
67		$encStart = $db->addQuotes( $db->timestamp( $start ) );
68		$encEnd = $db->addQuotes( $db->timestamp( $end ) );
69		$id = (int)$id; // cast NULL => 0 since rc_id is an integer
70
71		$res = $db->select(
72			'recentchanges',
73			[ 'rc_namespace', 'rc_title', 'rc_timestamp', 'rc_id' ],
74			[
75				$db->makeList( [
76					"rc_timestamp > $encStart",
77					"rc_timestamp = $encStart AND rc_id > " . $db->addQuotes( $id )
78				], LIST_OR ),
79				"rc_timestamp < $encEnd"
80			],
81			__METHOD__,
82			[ 'ORDER BY' => [ 'rc_timestamp ASC', 'rc_id ASC' ], 'LIMIT' => $limit ]
83		);
84
85		$events = [];
86		foreach ( $res as $row ) {
87			$events[] = [
88				'id' => (int)$row->rc_id,
89				'pos' => (int)wfTimestamp( TS_UNIX, $row->rc_timestamp ),
90				'item' => new TitleValue( (int)$row->rc_namespace, $row->rc_title )
91			];
92		}
93
94		return $events;
95	}
96
97	/**
98	 * Gets a list of important cache keys associated with a title
99	 *
100	 * @see WANObjectCacheRepear
101	 * @param WANObjectCache $cache
102	 * @param LinkTarget $t
103	 * @return string[]
104	 */
105	public function getEventAffectedKeys( WANObjectCache $cache, LinkTarget $t ) {
106		/** @var WikiPage[]|LocalFile[]|User[] $entities */
107		$entities = [];
108
109		// You can't create a WikiPage for special pages (-1) or other virtual
110		// namespaces, but special pages do appear in RC sometimes, e.g. for logs
111		// of AbuseFilter filter changes.
112		if ( $t->getNamespace() >= 0 ) {
113			$entities[] = MediaWikiServices::getInstance()->getWikiPageFactory()
114				->newFromLinkTarget( $t );
115		}
116
117		if ( $t->inNamespace( NS_FILE ) ) {
118			$entities[] = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
119				->newFile( $t->getText() );
120		}
121		if ( $t->inNamespace( NS_USER ) ) {
122			$entities[] = User::newFromName( $t->getText(), false );
123		}
124
125		$keys = [];
126		foreach ( $entities as $entity ) {
127			if ( $entity ) {
128				$keys = array_merge( $keys, $entity->getMutableCacheKeys( $cache ) );
129			}
130		}
131		if ( $keys ) {
132			$this->logger->debug( __CLASS__ . ': got key(s) ' . implode( ', ', $keys ) );
133		}
134
135		return $keys;
136	}
137}
138