MediaWiki master
ExternalStoreAccess.php
Go to the documentation of this file.
1<?php
22use Psr\Log\LoggerAwareInterface;
23use Psr\Log\LoggerInterface;
24use Psr\Log\NullLogger;
25use Wikimedia\RequestTimeout\TimeoutException;
26
44class ExternalStoreAccess implements LoggerAwareInterface {
46 private $storeFactory;
48 private $logger;
49
54 public function __construct( ExternalStoreFactory $factory, ?LoggerInterface $logger = null ) {
55 $this->storeFactory = $factory;
56 $this->logger = $logger ?: new NullLogger();
57 }
58
59 public function setLogger( LoggerInterface $logger ) {
60 $this->logger = $logger;
61 }
62
73 public function fetchFromURL( $url, array $params = [] ) {
74 return $this->storeFactory->getStoreForUrl( $url, $params )->fetchFromURL( $url );
75 }
76
87 public function fetchFromURLs( array $urls, array $params = [] ) {
88 $batches = $this->storeFactory->getUrlsByProtocol( $urls );
89 $retval = [];
90 foreach ( $batches as $proto => $batchedUrls ) {
91 $store = $this->storeFactory->getStore( $proto, $params );
92 $retval += $store->batchFetchFromURLs( $batchedUrls );
93 }
94 // invalid, not found, db dead, etc.
95 $missing = array_diff( $urls, array_keys( $retval ) );
96 foreach ( $missing as $url ) {
97 $retval[$url] = false;
98 }
99
100 return $retval;
101 }
102
118 public function insert( $data, array $params = [], ?array $tryStores = null ) {
119 $tryStores ??= $this->storeFactory->getWriteBaseUrls();
120 if ( !$tryStores ) {
121 throw new ExternalStoreException( "List of external stores provided is empty." );
122 }
123
124 $error = false; // track the last exception thrown
125 $readOnlyCount = 0; // track if a store was read-only
126 while ( count( $tryStores ) > 0 ) {
127 $index = mt_rand( 0, count( $tryStores ) - 1 );
128 $storeUrl = $tryStores[$index];
129
130 $this->logger->debug( __METHOD__ . ": trying $storeUrl" );
131
132 $store = $this->storeFactory->getStoreForUrl( $storeUrl, $params );
133 if ( $store === false ) {
134 throw new ExternalStoreException( "Invalid external storage protocol - $storeUrl" );
135 }
136
137 $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl );
138 try {
139 if ( $store->isReadOnly( $location ) ) {
140 $readOnlyCount++;
141 $msg = 'read only';
142 } else {
143 $url = $store->store( $location, $data );
144 if ( $url !== false && $url !== '' ) {
145 // A store accepted the write; done!
146 return $url;
147 }
148 throw new ExternalStoreException(
149 "No URL returned by storage medium ($storeUrl)"
150 );
151 }
152 } catch ( TimeoutException $e ) {
153 throw $e;
154 } catch ( Exception $ex ) {
155 $error = $ex;
156 $msg = 'caught ' . get_class( $error ) . ' exception: ' . $error->getMessage();
157 }
158
159 unset( $tryStores[$index] ); // Don't try this one again!
160 $tryStores = array_values( $tryStores ); // Must have consecutive keys
161 $this->logger->error(
162 "Unable to store text to external storage {store_path} ({failure})",
163 [ 'store_path' => $storeUrl, 'failure' => $msg ]
164 );
165 }
166
167 // We only get here when all stores failed.
168 if ( $error ) {
169 // At least one store threw an exception. Re-throw the most recent one.
170 throw $error;
171 } elseif ( $readOnlyCount ) {
172 // If no exceptions where thrown and we get here,
173 // this should mean that all stores were in read-only mode.
174 throw new ReadOnlyError();
175 } else {
176 // We shouldn't get here. If there were no failures, this method should have returned
177 // from inside the body of the loop.
178 throw new LogicException( "Unexpected failure to store text to external store" );
179 }
180 }
181
187 public function isReadOnly( $storeUrls = null ) {
188 if ( $storeUrls === null ) {
189 $storeUrls = $this->storeFactory->getWriteBaseUrls();
190 } else {
191 $storeUrls = is_array( $storeUrls ) ? $storeUrls : [ $storeUrls ];
192 }
193
194 if ( !$storeUrls ) {
195 return false; // no stores exists which can be "read only"
196 }
197
198 foreach ( $storeUrls as $storeUrl ) {
199 $store = $this->storeFactory->getStoreForUrl( $storeUrl );
200 $location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl );
201 if ( $store !== false && !$store->isReadOnly( $location ) ) {
202 return false; // at least one store is not read-only
203 }
204 }
205
206 return true; // all stores are read-only
207 }
208}
This is the main interface for fetching or inserting objects with ExternalStore.
insert( $data, array $params=[], ?array $tryStores=null)
Insert data into storage and return the assigned URL.
__construct(ExternalStoreFactory $factory, ?LoggerInterface $logger=null)
setLogger(LoggerInterface $logger)
fetchFromURL( $url, array $params=[])
Fetch data from given URL.
fetchFromURLs(array $urls, array $params=[])
Fetch data from multiple URLs with a minimum of round trips.
isReadOnly( $storeUrls=null)
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...