3 * @defgroup ExternalStorage ExternalStorage
6 use \Psr\Log\LoggerAwareInterface
;
7 use \Psr\Log\LoggerInterface
;
8 use \Psr\Log\NullLogger
;
11 * Key/value blob storage for a collection of storage medium types (e.g. RDBMs, files)
13 * Multiple medium types can be active and each one can have multiple "locations" available.
14 * Blobs are stored under URLs of the form "<protocol>://<location>/<path>". Each type of storage
15 * medium has an associated protocol. Insertions will randomly pick mediums and locations from
16 * the provided list of writable medium-qualified locations. Insertions will also fail-over to
17 * other writable locations or mediums if one or more are not available.
19 * @ingroup ExternalStorage
22 class ExternalStoreAccess
implements LoggerAwareInterface
{
23 /** @var ExternalStoreFactory */
24 private $storeFactory;
25 /** @var LoggerInterface */
29 * @param ExternalStoreFactory $factory
30 * @param LoggerInterface|null $logger
32 public function __construct( ExternalStoreFactory
$factory, LoggerInterface
$logger = null ) {
33 $this->storeFactory
= $factory;
34 $this->logger
= $logger ?
: new NullLogger();
37 public function setLogger( LoggerInterface
$logger ) {
38 $this->logger
= $logger;
42 * Fetch data from given URL
44 * @see ExternalStoreFactory::getStore()
46 * @param string $url The URL of the text to get
47 * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
48 * @return string|bool The text stored or false on error
49 * @throws ExternalStoreException
51 public function fetchFromURL( $url, array $params = [] ) {
52 return $this->storeFactory
->getStoreForUrl( $url, $params )->fetchFromURL( $url );
56 * Fetch data from multiple URLs with a minimum of round trips
58 * @see ExternalStoreFactory::getStore()
60 * @param array $urls The URLs of the text to get
61 * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
62 * @return array Map of (url => string or false if not found)
63 * @throws ExternalStoreException
65 public function fetchFromURLs( array $urls, array $params = [] ) {
66 $batches = $this->storeFactory
->getUrlsByProtocol( $urls );
68 foreach ( $batches as $proto => $batchedUrls ) {
69 $store = $this->storeFactory
->getStore( $proto, $params );
70 $retval +
= $store->batchFetchFromURLs( $batchedUrls );
72 // invalid, not found, db dead, etc.
73 $missing = array_diff( $urls, array_keys( $retval ) );
74 foreach ( $missing as $url ) {
75 $retval[$url] = false;
82 * Insert data into storage and return the assigned URL
84 * This will randomly pick one of the available write storage locations to put the data.
85 * It will keep failing-over to any untried storage locations whenever one location is
88 * @see ExternalStoreFactory::getStore()
91 * @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
92 * @param string[]|null $tryStores Base URLs to try, e.g. [ "DB://cluster1" ]
93 * @return string|bool The URL of the stored data item, or false on error
94 * @throws ExternalStoreException
96 public function insert( $data, array $params = [], array $tryStores = null ) {
97 $tryStores = $tryStores ??
$this->storeFactory
->getWriteBaseUrls();
99 throw new ExternalStoreException( "List of external stores provided is empty." );
103 while ( count( $tryStores ) > 0 ) {
104 $index = mt_rand( 0, count( $tryStores ) - 1 );
105 $storeUrl = $tryStores[$index];
107 $this->logger
->debug( __METHOD__
. ": trying $storeUrl\n" );
109 $store = $this->storeFactory
->getStoreForUrl( $storeUrl, $params );
110 if ( $store === false ) {
111 throw new ExternalStoreException( "Invalid external storage protocol - $storeUrl" );
114 $location = $this->storeFactory
->getStoreLocationFromUrl( $storeUrl );
116 if ( $store->isReadOnly( $location ) ) {
119 $url = $store->store( $location, $data );
120 if ( strlen( $url ) ) {
121 return $url; // a store accepted the write; done!
123 $msg = 'operation failed';
125 } catch ( Exception
$error ) {
126 $msg = 'caught ' . get_class( $error ) . ' exception: ' . $error->getMessage();
129 unset( $tryStores[$index] ); // Don't try this one again!
130 $tryStores = array_values( $tryStores ); // Must have consecutive keys
131 $this->logger
->error(
132 "Unable to store text to external storage {store_path} ({failure})",
133 [ 'store_path' => $storeUrl, 'failure' => $msg ]
138 throw $error; // rethrow the last error
140 throw new ExternalStoreException( "Unable to store text to external storage" );
145 * @param string[]|string|null $storeUrls Base URL(s) to check, e.g. [ "DB://cluster1" ]
146 * @return bool Whether all the default insertion stores are marked as read-only
147 * @throws ExternalStoreException
149 public function isReadOnly( $storeUrls = null ) {
150 if ( $storeUrls === null ) {
151 $storeUrls = $this->storeFactory
->getWriteBaseUrls();
153 $storeUrls = is_array( $storeUrls ) ?
$storeUrls : [ $storeUrls ];
157 return false; // no stores exists which can be "read only"
160 foreach ( $storeUrls as $storeUrl ) {
161 $store = $this->storeFactory
->getStoreForUrl( $storeUrl );
162 $location = $this->storeFactory
->getStoreLocationFromUrl( $storeUrl );
163 if ( $store !== false && !$store->isReadOnly( $location ) ) {
164 return false; // at least one store is not read-only
168 return true; // all stores are read-only