MediaWiki master
MultiBackendSessionStore.php
Go to the documentation of this file.
1<?php
8namespace MediaWiki\Session;
9
11use Psr\Log\LoggerInterface;
12use RuntimeException;
16
48
49 private const STATS_LABEL_ANON = 'anonymous';
50 private const STATS_LABEL_AUTH = 'authenticated';
51
52 private BagOStuff $anonSessionStore;
53 private BagOStuff $authenticatedSessionStore;
55 private bool $sameBackend;
56 private StatsFactory $statsFactory;
57
62 private LoggerInterface $logger;
63
64 public function __construct(
65 BagOStuff $anonSessionStore,
66 BagOStuff $authenticatedSessionStore,
67 LoggerInterface $logger,
68 StatsFactory $statsFactory
69 ) {
70 $this->setLogger( $logger );
71 // Do some logging of the individual stores before wrapping.
72 $this->logger->debug( 'SessionManager using anon store ' . get_class( $anonSessionStore ) );
73 $this->logger->debug( 'SessionManager using auth store ' . get_class( $authenticatedSessionStore ) );
74
75 $this->sameBackend = $anonSessionStore === $authenticatedSessionStore;
76 $this->anonSessionStore = $this->wrapWithCachedBagOStuff( $anonSessionStore );
77 $this->authenticatedSessionStore = $this->wrapWithCachedBagOStuff( $authenticatedSessionStore );
78 $this->statsFactory = $statsFactory;
79 }
80
82 public function setLogger( LoggerInterface $logger ): void {
83 $this->logger = $logger;
84 }
85
86 private function wrapWithCachedBagOStuff( BagOStuff $store ): CachedBagOStuff {
87 if ( $store instanceof CachedBagOStuff ) {
88 return $store;
89 }
90
91 return new CachedBagOStuff( $store );
92 }
93
102 private function getActiveStore( SessionInfo $sessionInfo ): array {
103 $userInfo = $sessionInfo->getUserInfo();
104 $anonData = null;
105
106 if ( $userInfo === null ) {
107 $this->logger->debug( 'No user info found for this session', [
108 'sessionInfo' => (string)$sessionInfo,
109 'exception' => new RuntimeException(),
110 ] );
111
112 // We don't yet know whether this is an anonymous or authenticated session
113 // (e.g., getSessionById() is being used), so we need to check both stores.
114 $anonKey = $this->anonSessionStore->makeKey( 'MWSession', $sessionInfo->getId() );
115 $authKey = $this->authenticatedSessionStore->makeKey( 'MWSession', $sessionInfo->getId() );
116
117 $authData = $this->authenticatedSessionStore->get( $authKey );
118 if ( !$this->authenticatedSessionStore->wasLastGetCached() ) {
119 $this->statsFactory->getCounter( 'sessionstore_nouserinfo_get_total' )
120 ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
121 ->setLabel( 'type', 'authenticated' )
122 ->setLabel( 'status', $authData ? 'hit' : 'miss' )
123 ->increment();
124 }
125 if ( !$authData ) {
126 $anonData = $this->anonSessionStore->get( $anonKey );
127 if ( !$this->anonSessionStore->wasLastGetCached() ) {
128 $this->statsFactory->getCounter( 'sessionstore_nouserinfo_get_total' )
129 ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
130 ->setLabel( 'type', 'anonymous' )
131 ->setLabel( 'status', $anonData ? 'hit' : 'miss' )
132 ->increment();
133 }
134 }
135
136 $anonUserName = $anonData['metadata']['userName'] ?? null;
137 $authUserName = $authData['metadata']['userName'] ?? null;
138
139 if ( $anonData && $anonUserName !== null ) {
140 // The data does not match the store!
141 // This is actually expected when the two stores are the same
142 // (which is useful for testing in production).
143 if ( !$this->sameBackend ) {
144 $this->logger->warning( 'No userInfo: authenticated data should not be in the anonymous store', [
145 'sessionInfo' => (string)$sessionInfo,
146 'exception' => new RuntimeException(),
147 ] );
148 }
149
150 $anonData = false;
151 $anonUserName = null;
152 }
153
154 if ( $authData && $authUserName === null ) {
155 if ( !$this->sameBackend ) {
156 $this->logger->warning( 'No userInfo: anonymous data should not be in the authenticated store', [
157 'sessionInfo' => (string)$sessionInfo,
158 'exception' => new RuntimeException(),
159 ] );
160 }
161 $authData = false;
162 }
163
164 if ( $anonData && $authData && !$this->sameBackend ) {
165 $this->logger->warning( 'Both stores should not have the same session data', [
166 'sessionInfo' => (string)$sessionInfo,
167 'exception' => new RuntimeException(),
168 ] );
169 }
170
171 if ( $authData ) {
172 return [ $this->authenticatedSessionStore, true ];
173 }
174
175 return [ $this->anonSessionStore, false ];
176 }
177
178 if ( !$userInfo->isAnon() ) {
179 // Hopefully, we can use the user info here to know if it's an anonymous user or
180 // and authenticated user. This will help us determine which store to use.
181 return [ $this->authenticatedSessionStore, true ];
182 }
183
184 return [ $this->anonSessionStore, false ];
185 }
186
195 public function get( SessionInfo $info ) {
196 $this->logger->debug( __METHOD__ . " was called for $info" );
197
198 [ $store, $isAuthenticated ] = $this->getActiveStore( $info );
199
200 $key = $store->makeKey( 'MWSession', $info->getId() );
201 $data = $store->get( $key );
202 $userName = $data['metadata']['userName'] ?? null;
203
204 if ( $isAuthenticated && $data && $userName === null ) {
205 $this->logger->warning( 'Store is authenticated, but the user associated to the data is anonymous', [
206 'sessionInfo' => (string)$info,
207 'user' => (string)$info->getUserInfo(),
208 'exception' => new RuntimeException(),
209 ] );
210 }
211
212 if ( !$isAuthenticated && $userName !== null ) {
213 $this->logger->warning( 'Store is anonymous, but the user associated to the data is authenticated', [
214 'sessionInfo' => (string)$info,
215 'user' => (string)$info->getUserInfo(),
216 'exception' => new RuntimeException(),
217 ] );
218 }
219
220 // We are interested in tracking reads from the actual store (uncached in-process)
221 // to know how many times we do actual store look-ups.
222 if ( !$store->wasLastGetCached() ) {
223 $this->statsFactory->getCounter( 'sessionstore_get_total' )
224 ->setLabel( 'type', $isAuthenticated ? self::STATS_LABEL_AUTH : self::STATS_LABEL_ANON )
225 ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
226 ->increment();
227 }
228
229 return $data;
230 }
231
241 public function set( SessionInfo $info, $value, $exptime = 0, $flags = 0 ): void {
242 $this->logger->debug( __METHOD__ . " was called for $info" );
243
244 if ( $flags === BagOStuff::WRITE_CACHE_ONLY && $value === false ) {
245 // writing $value === false in the cache is only used by
246 // SessionManager::generateSessionId() to prevent an unnecessary store lookup.
247 // We don't know which store the ID is going to be used for in that case,
248 // so need to avoid calling getActiveStore() which would result in a store lookup
249 // and defeat the purpose. Since a new random key can't conflict with existing keys,
250 // no harm in just updating both caches.
251 $anonKey = $this->anonSessionStore->makeKey( 'MWSession', $info->getId() );
252 $authKey = $this->authenticatedSessionStore->makeKey( 'MWSession', $info->getId() );
253
254 $this->anonSessionStore->set( $anonKey, false, 0, BagOStuff::WRITE_CACHE_ONLY );
255 $this->authenticatedSessionStore->set( $authKey, false, 0, BagOStuff::WRITE_CACHE_ONLY );
256 return;
257 }
258
259 [ $store, $isAuthenticated ] = $this->getActiveStore( $info );
260
261 $key = $store->makeKey( 'MWSession', $info->getId() );
262 $userName = $value['metadata']['userName'] ?? null;
263
264 // SessionManager::generateSessionId() can perform a cache warming
265 // operation by setting `false` as the cache value. We don't want to
266 // log those.
267 if ( $value ) {
268 if ( $isAuthenticated && $userName === null ) {
269 $this->logger->warning( 'Store is authenticated, but the user associated to the data is anonymous', [
270 'sessionInfo' => (string)$info,
271 'user' => (string)$info->getUserInfo(),
272 'exception' => new RuntimeException(),
273 'flags' => (string)$flags
274 ] );
275 }
276
277 if ( !$isAuthenticated && $userName !== null ) {
278 $this->logger->warning( 'Store is anonymous, but the user associated to the data is authenticated', [
279 'sessionInfo' => (string)$info,
280 'user' => (string)$info->getUserInfo(),
281 'exception' => new RuntimeException(),
282 'flags' => (string)$flags
283 ] );
284 }
285 }
286
287 $store->set( $key, $value, $exptime, $flags );
288
289 if ( ( $flags & BagOStuff::WRITE_CACHE_ONLY ) === 0 ) {
290 $this->statsFactory->getCounter( 'sessionstore_set_total' )
291 ->setLabel( 'type', $isAuthenticated ? self::STATS_LABEL_AUTH : self::STATS_LABEL_ANON )
292 ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
293 ->increment();
294 }
295 }
296
302 public function delete( SessionInfo $info ): void {
303 $this->logger->debug( __METHOD__ . " was called for $info" );
304
305 [ $store, $isAuthenticated ] = $this->getActiveStore( $info );
306 $key = $store->makeKey( 'MWSession', $info->getId() );
307
308 $store->delete( $key );
309
310 $this->statsFactory->getCounter( 'sessionstore_delete_total' )
311 ->setLabel( 'type', $isAuthenticated ? self::STATS_LABEL_AUTH : self::STATS_LABEL_ANON )
312 ->setLabel( 'wiki', WikiMap::getCurrentWikiId() )
313 ->increment();
314 }
315
319 public function shutdown(): void {
320 if ( random_int( 1, 100 ) === 1 ) {
321 $this->logger->debug( 'Cleaning session store expired entries' );
322 $timeNow = wfTimestampNow();
323 $this->authenticatedSessionStore->deleteObjectsExpiringBefore( $timeNow );
324 $this->anonSessionStore->deleteObjectsExpiringBefore( $timeNow );
325 }
326 }
327}
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
An implementation of a session store with two backends for storing anonymous and authenticated sessio...
shutdown()
Will be called during shutdown.void
__construct(BagOStuff $anonSessionStore, BagOStuff $authenticatedSessionStore, LoggerInterface $logger, StatsFactory $statsFactory)
Value object returned by SessionProvider.
getId()
Return the session ID.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73
Wrap any BagOStuff and add an in-process memory cache to it.
This is the primary interface for validating metrics definitions, caching defined metrics,...
This is a session store abstraction layer, which can be used to read and write sessions to configured...
setLabel(string $key, string $value)