MediaWiki  1.30.0
EtcdConfig.php
Go to the documentation of this file.
1 <?php
21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerInterface;
23 use Wikimedia\WaitConditionLoop;
24 
30 class EtcdConfig implements Config, LoggerAwareInterface {
32  private $http;
34  private $srvCache;
36  private $procCache;
38  private $logger;
39 
41  private $host;
43  private $protocol;
45  private $directory;
47  private $encoding;
49  private $baseCacheTTL;
51  private $skewCacheTTL;
53  private $timeout;
54 
67  public function __construct( array $params ) {
68  $params += [
69  'protocol' => 'http',
70  'encoding' => 'JSON',
71  'cacheTTL' => 10,
72  'skewTTL' => 1,
73  'timeout' => 2
74  ];
75 
76  $this->host = $params['host'];
77  $this->protocol = $params['protocol'];
78  $this->directory = trim( $params['directory'], '/' );
79  $this->encoding = $params['encoding'];
80  $this->skewCacheTTL = $params['skewTTL'];
81  $this->baseCacheTTL = max( $params['cacheTTL'] - $this->skewCacheTTL, 0 );
82  $this->timeout = $params['timeout'];
83 
84  if ( !isset( $params['cache'] ) ) {
85  $this->srvCache = new HashBagOStuff();
86  } elseif ( $params['cache'] instanceof BagOStuff ) {
87  $this->srvCache = $params['cache'];
88  } else {
89  $this->srvCache = ObjectFactory::getObjectFromSpec( $params['cache'] );
90  }
91 
92  $this->logger = new Psr\Log\NullLogger();
93  $this->http = new MultiHttpClient( [
94  'connTimeout' => $this->timeout,
95  'reqTimeout' => $this->timeout,
96  'logger' => $this->logger
97  ] );
98  }
99 
100  public function setLogger( LoggerInterface $logger ) {
101  $this->logger = $logger;
102  $this->http->setLogger( $logger );
103  }
104 
105  public function has( $name ) {
106  $this->load();
107 
108  return array_key_exists( $name, $this->procCache['config'] );
109  }
110 
111  public function get( $name ) {
112  $this->load();
113 
114  if ( !array_key_exists( $name, $this->procCache['config'] ) ) {
115  throw new ConfigException( "No entry found for '$name'." );
116  }
117 
118  return $this->procCache['config'][$name];
119  }
120 
124  private function load() {
125  if ( $this->procCache !== null ) {
126  return; // already loaded
127  }
128 
129  $now = microtime( true );
130  $key = $this->srvCache->makeGlobalKey(
131  __CLASS__,
132  $this->host,
133  $this->directory
134  );
135 
136  // Get the cached value or block until it is regenerated (by this or another thread)...
137  $data = null; // latest config info
138  $error = null; // last error message
139  $loop = new WaitConditionLoop(
140  function () use ( $key, $now, &$data, &$error ) {
141  // Check if the values are in cache yet...
142  $data = $this->srvCache->get( $key );
143  if ( is_array( $data ) && $data['expires'] > $now ) {
144  $this->logger->debug( "Found up-to-date etcd configuration cache." );
145 
146  return WaitConditionLoop::CONDITION_REACHED;
147  }
148 
149  // Cache is either empty or stale;
150  // refresh the cache from etcd, using a mutex to reduce stampedes...
151  if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
152  try {
153  list( $config, $error, $retry ) = $this->fetchAllFromEtcd();
154  if ( is_array( $config ) ) {
155  // Avoid having all servers expire cache keys at the same time
156  $expiry = microtime( true ) + $this->baseCacheTTL;
157  $expiry += mt_rand( 0, 1e6 ) / 1e6 * $this->skewCacheTTL;
158 
159  $data = [ 'config' => $config, 'expires' => $expiry ];
160  $this->srvCache->set( $key, $data, BagOStuff::TTL_INDEFINITE );
161 
162  $this->logger->info( "Refreshed stale etcd configuration cache." );
163 
164  return WaitConditionLoop::CONDITION_REACHED;
165  } else {
166  $this->logger->error( "Failed to fetch configuration: $error" );
167  if ( !$retry ) {
168  // Fail fast since the error is likely to keep happening
169  return WaitConditionLoop::CONDITION_FAILED;
170  }
171  }
172  } finally {
173  $this->srvCache->unlock( $key ); // release mutex
174  }
175  }
176 
177  if ( is_array( $data ) ) {
178  $this->logger->info( "Using stale etcd configuration cache." );
179 
180  return WaitConditionLoop::CONDITION_REACHED;
181  }
182 
183  return WaitConditionLoop::CONDITION_CONTINUE;
184  },
186  );
187 
188  if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) {
189  // No cached value exists and etcd query failed; throw an error
190  throw new ConfigException( "Failed to load configuration from etcd: $error" );
191  }
192 
193  $this->procCache = $data;
194  }
195 
199  public function fetchAllFromEtcd() {
200  $dsd = new DnsSrvDiscoverer( $this->host );
201  $servers = $dsd->getServers();
202  if ( !$servers ) {
203  return $this->fetchAllFromEtcdServer( $this->host );
204  }
205 
206  do {
207  // Pick a random etcd server from dns
208  $server = $dsd->pickServer( $servers );
209  $host = IP::combineHostAndPort( $server['target'], $server['port'] );
210  // Try to load the config from this particular server
211  list( $config, $error, $retry ) = $this->fetchAllFromEtcdServer( $host );
212  if ( is_array( $config ) || !$retry ) {
213  break;
214  }
215 
216  // Avoid the server next time if that failed
217  $servers = $dsd->removeServer( $server, $servers );
218  } while ( $servers );
219 
220  return [ $config, $error, $retry ];
221  }
222 
227  protected function fetchAllFromEtcdServer( $address ) {
228  // Retrieve all the values under the MediaWiki config directory
229  list( $rcode, $rdesc, /* $rhdrs */, $rbody, $rerr ) = $this->http->run( [
230  'method' => 'GET',
231  'url' => "{$this->protocol}://{$address}/v2/keys/{$this->directory}/?recursive=true",
232  'headers' => [ 'content-type' => 'application/json' ]
233  ] );
234 
235  static $terminalCodes = [ 404 => true ];
236  if ( $rcode < 200 || $rcode > 399 ) {
237  return [
238  null,
239  strlen( $rerr ) ? $rerr : "HTTP $rcode ($rdesc)",
240  empty( $terminalCodes[$rcode] )
241  ];
242  }
243  try {
244  return [ $this->parseResponse( $rbody ), null, false ];
245  } catch ( EtcdConfigParseError $e ) {
246  return [ null, $e->getMessage(), false ];
247  }
248  }
249 
256  protected function parseResponse( $rbody ) {
257  $info = json_decode( $rbody, true );
258  if ( $info === null ) {
259  throw new EtcdConfigParseError( "Error unserializing JSON response." );
260  }
261  if ( !isset( $info['node'] ) || !is_array( $info['node'] ) ) {
262  throw new EtcdConfigParseError(
263  "Unexpected JSON response: Missing or invalid node at top level." );
264  }
265  $config = [];
266  $this->parseDirectory( '', $info['node'], $config );
267  return $config;
268  }
269 
278  protected function parseDirectory( $dirName, $dirNode, &$config ) {
279  if ( !isset( $dirNode['nodes'] ) ) {
280  throw new EtcdConfigParseError(
281  "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
282  }
283  if ( !is_array( $dirNode['nodes'] ) ) {
284  throw new EtcdConfigParseError(
285  "Unexpected JSON response in dir '$dirName'; 'nodes' is not an array." );
286  }
287 
288  foreach ( $dirNode['nodes'] as $node ) {
289  $baseName = basename( $node['key'] );
290  $fullName = $dirName === '' ? $baseName : "$dirName/$baseName";
291  if ( !empty( $node['dir'] ) ) {
292  $this->parseDirectory( $fullName, $node, $config );
293  } else {
294  $value = $this->unserialize( $node['value'] );
295  if ( !is_array( $value ) || !array_key_exists( 'val', $value ) ) {
296  throw new EtcdConfigParseError( "Failed to parse value for '$fullName'." );
297  }
298 
299  $config[$fullName] = $value['val'];
300  }
301  }
302  }
303 
308  private function unserialize( $string ) {
309  if ( $this->encoding === 'YAML' ) {
310  return yaml_parse( $string );
311  } else { // JSON
312  return json_decode( $string, true );
313  }
314  }
315 }
DnsSrvDiscoverer
Definition: DnsSrvDiscoverer.php:26
EtcdConfig\$http
MultiHttpClient $http
Definition: EtcdConfig.php:32
MultiHttpClient
Class to handle concurrent HTTP requests.
Definition: MultiHttpClient.php:48
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
directory
The most up to date schema for the tables in the database will always be tables sql in the maintenance directory
Definition: schema.txt:2
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:31
EtcdConfig\__construct
__construct(array $params)
Definition: EtcdConfig.php:67
IP\combineHostAndPort
static combineHostAndPort( $host, $port, $defaultPort=false)
Given a host name and a port, combine them into host/port string like you might find in a URL.
Definition: IP.php:315
EtcdConfig\fetchAllFromEtcd
fetchAllFromEtcd()
Definition: EtcdConfig.php:199
EtcdConfig\fetchAllFromEtcdServer
fetchAllFromEtcdServer( $address)
Definition: EtcdConfig.php:227
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
EtcdConfig
Interface for configuration instances.
Definition: EtcdConfig.php:30
$params
$params
Definition: styleTest.css.php:40
BagOStuff
interface is intended to be more or less compatible with the PHP memcached client.
Definition: BagOStuff.php:47
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
http
Apache License January http
Definition: APACHE-LICENSE-2.0.txt:3
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
EtcdConfig\load
load()
Definition: EtcdConfig.php:124
EtcdConfig\$directory
string $directory
Definition: EtcdConfig.php:45
EtcdConfig\setLogger
setLogger(LoggerInterface $logger)
Definition: EtcdConfig.php:100
IExpiringStore\TTL_INDEFINITE
const TTL_INDEFINITE
Definition: IExpiringStore.php:44
ConfigException
Exceptions for config failures.
Definition: ConfigException.php:28
EtcdConfig\$logger
LoggerInterface $logger
Definition: EtcdConfig.php:38
EtcdConfig\$skewCacheTTL
int $skewCacheTTL
Definition: EtcdConfig.php:51
EtcdConfig\unserialize
unserialize( $string)
Definition: EtcdConfig.php:308
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
EtcdConfig\$procCache
array $procCache
Definition: EtcdConfig.php:36
$e
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2141
EtcdConfig\parseDirectory
parseDirectory( $dirName, $dirNode, &$config)
Recursively parse a directory node and populate the array passed by reference, throwing EtcdConfigPar...
Definition: EtcdConfig.php:278
$value
$value
Definition: styleTest.css.php:45
EtcdConfig\$baseCacheTTL
int $baseCacheTTL
Definition: EtcdConfig.php:49
ObjectFactory\getObjectFromSpec
static getObjectFromSpec( $spec)
Instantiate an object based on a specification array.
Definition: ObjectFactory.php:58
EtcdConfig\$srvCache
BagOStuff $srvCache
Definition: EtcdConfig.php:34
EtcdConfig\$host
string $host
Definition: EtcdConfig.php:41
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
EtcdConfig\$protocol
string $protocol
Definition: EtcdConfig.php:43
EtcdConfig\has
has( $name)
Definition: EtcdConfig.php:105
true
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1965
EtcdConfig\$timeout
int $timeout
Definition: EtcdConfig.php:53
EtcdConfig\$encoding
string $encoding
Definition: EtcdConfig.php:47
EtcdConfig\parseResponse
parseResponse( $rbody)
Parse a response body, throwing EtcdConfigParseError if there is a validation error.
Definition: EtcdConfig.php:256
EtcdConfigParseError
Definition: EtcdConfigParseError.php:3
array
the array() calling protocol came about after MediaWiki 1.4rc1.