Completed
Branch master (4cbefc)
by
unknown
27:08
created

ChronologyProtector::getTouchedKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Generator of database load balancing objects.
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 Database
22
 */
23
24
/**
25
 * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
26
 * Kind of like Hawking's [[Chronology Protection Agency]].
27
 */
28
class ChronologyProtector {
29
	/** @var BagOStuff */
30
	protected $store;
31
32
	/** @var string Storage key name */
33
	protected $key;
34
	/** @var string Hash of client parameters */
35
	protected $clientId;
36
	/** @var bool Whether to no-op all method calls */
37
	protected $enabled = true;
38
	/** @var bool Whether to check and wait on positions */
39
	protected $wait = true;
40
41
	/** @var bool Whether the client data was loaded */
42
	protected $initialized = false;
43
	/** @var DBMasterPos[] Map of (DB master name => position) */
44
	protected $startupPositions = [];
45
	/** @var DBMasterPos[] Map of (DB master name => position) */
46
	protected $shutdownPositions = [];
47
	/** @var float[] Map of (DB master name => 1) */
48
	protected $shutdownTouchDBs = [];
49
50
	/**
51
	 * @param BagOStuff $store
52
	 * @param array $client Map of (ip: <IP>, agent: <user-agent>)
53
	 * @since 1.27
54
	 */
55
	public function __construct( BagOStuff $store, array $client ) {
56
		$this->store = $store;
57
		$this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
58
		$this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
59
	}
60
61
	/**
62
	 * @param bool $enabled Whether to no-op all method calls
63
	 * @since 1.27
64
	 */
65
	public function setEnabled( $enabled ) {
66
		$this->enabled = $enabled;
67
	}
68
69
	/**
70
	 * @param bool $enabled Whether to check and wait on positions
71
	 * @since 1.27
72
	 */
73
	public function setWaitEnabled( $enabled ) {
74
		$this->wait = $enabled;
75
	}
76
77
	/**
78
	 * Initialise a LoadBalancer to give it appropriate chronology protection.
79
	 *
80
	 * If the stash has a previous master position recorded, this will try to
81
	 * make sure that the next query to a replica DB of that master will see changes up
82
	 * to that position by delaying execution. The delay may timeout and allow stale
83
	 * data if no non-lagged replica DBs are available.
84
	 *
85
	 * @param LoadBalancer $lb
86
	 * @return void
87
	 */
88
	public function initLB( LoadBalancer $lb ) {
89
		if ( !$this->enabled || $lb->getServerCount() <= 1 ) {
90
			return; // non-replicated setup or disabled
91
		}
92
93
		$this->initPositions();
94
95
		$masterName = $lb->getServerName( $lb->getWriterIndex() );
96
		if ( !empty( $this->startupPositions[$masterName] ) ) {
97
			$pos = $this->startupPositions[$masterName];
98
			wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
99
			$lb->waitFor( $pos );
100
		}
101
	}
102
103
	/**
104
	 * Notify the ChronologyProtector that the LoadBalancer is about to shut
105
	 * down. Saves replication positions.
106
	 *
107
	 * @param LoadBalancer $lb
108
	 * @return void
109
	 */
110
	public function shutdownLB( LoadBalancer $lb ) {
111
		if ( !$this->enabled ) {
112
			return; // not enabled
113
		} elseif ( !$lb->hasOrMadeRecentMasterChanges( INF ) ) {
114
			// Only save the position if writes have been done on the connection
115
			return;
116
		}
117
118
		$masterName = $lb->getServerName( $lb->getWriterIndex() );
119
		if ( $lb->getServerCount() > 1 ) {
120
			$pos = $lb->getMasterPos();
121
			wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
122
			$this->shutdownPositions[$masterName] = $pos;
123
		} else {
124
			wfDebugLog( 'replication', __METHOD__ . ": DB '$masterName' touched\n" );
125
		}
126
		$this->shutdownTouchDBs[$masterName] = 1;
127
	}
128
129
	/**
130
	 * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
131
	 * May commit chronology data to persistent storage.
132
	 *
133
	 * @return DBMasterPos[] Empty on success; returns the (db name => position) map on failure
134
	 */
135
	public function shutdown() {
136
		if ( !$this->enabled ) {
137
			return [];
138
		}
139
140
		// Some callers might want to know if a user recently touched a DB.
141
		// These writes do not need to block on all datacenters receiving them.
142
		foreach ( $this->shutdownTouchDBs as $dbName => $unused ) {
143
			$this->store->set(
144
				$this->getTouchedKey( $this->store, $dbName ),
145
				microtime( true ),
146
				BagOStuff::TTL_DAY
147
			);
148
		}
149
150
		if ( !count( $this->shutdownPositions ) ) {
151
			return []; // nothing to save
152
		}
153
154
		wfDebugLog( 'replication',
155
			__METHOD__ . ": saving master pos for " .
156
			implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
157
		);
158
159
		// CP-protected writes should overwhemingly go to the master datacenter, so get DC-local
160
		// lock to merge the values. Use a DC-local get() and a synchronous all-DC set(). This
161
		// makes it possible for the BagOStuff class to write in parallel to all DCs with one RTT.
162
		if ( $this->store->lock( $this->key, 3 ) ) {
163
			$ok = $this->store->set(
164
				$this->key,
165
				self::mergePositions( $this->store->get( $this->key ), $this->shutdownPositions ),
166
				BagOStuff::TTL_MINUTE,
167
				BagOStuff::WRITE_SYNC
168
			);
169
			$this->store->unlock( $this->key );
170
		} else {
171
			$ok = false;
172
		}
173
174
		if ( !$ok ) {
175
			// Raced out too many times or stash is down
176
			wfDebugLog( 'replication',
177
				__METHOD__ . ": failed to save master pos for " .
178
				implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
179
			);
180
181
			return $this->shutdownPositions;
182
		}
183
184
		return [];
185
	}
186
187
	/**
188
	 * @param string $dbName DB master name (e.g. "db1052")
189
	 * @return float|bool UNIX timestamp when client last touched the DB; false if not on record
190
	 * @since 1.28
191
	 */
192
	public function getTouched( $dbName ) {
193
		return $this->store->get( $this->getTouchedKey( $this->store, $dbName ) );
194
	}
195
196
	/**
197
	 * @param BagOStuff $store
198
	 * @param string $dbName
199
	 * @return string
200
	 */
201
	private function getTouchedKey( BagOStuff $store, $dbName ) {
202
		return $store->makeGlobalKey( __CLASS__, 'mtime', $this->clientId, $dbName );
203
	}
204
205
	/**
206
	 * Load in previous master positions for the client
207
	 */
208
	protected function initPositions() {
209
		if ( $this->initialized ) {
210
			return;
211
		}
212
213
		$this->initialized = true;
214
		if ( $this->wait ) {
215
			$data = $this->store->get( $this->key );
216
			$this->startupPositions = $data ? $data['positions'] : [];
217
			wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (read)\n" );
218
		} else {
219
			$this->startupPositions = [];
220
			wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (unread)\n" );
221
		}
222
	}
223
224
	/**
225
	 * @param array|bool $curValue
226
	 * @param DBMasterPos[] $shutdownPositions
227
	 * @return array
228
	 */
229
	private static function mergePositions( $curValue, array $shutdownPositions ) {
230
		/** @var $curPositions DBMasterPos[] */
231
		if ( $curValue === false ) {
232
			$curPositions = $shutdownPositions;
233
		} else {
234
			$curPositions = $curValue['positions'];
235
			// Use the newest positions for each DB master
236
			foreach ( $shutdownPositions as $db => $pos ) {
237
				if ( !isset( $curPositions[$db] )
238
					|| $pos->asOfTime() > $curPositions[$db]->asOfTime()
239
				) {
240
					$curPositions[$db] = $pos;
241
				}
242
			}
243
		}
244
245
		return [ 'positions' => $curPositions ];
246
	}
247
}
248