21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerInterface;
23 use Wikimedia\WaitConditionLoop;
30 class EtcdConfig implements Config, LoggerAwareInterface {
77 $this->protocol =
$params[
'protocol'];
79 $this->encoding =
$params[
'encoding'];
80 $this->skewCacheTTL =
$params[
'skewTTL'];
81 $this->baseCacheTTL = max(
$params[
'cacheTTL'] - $this->skewCacheTTL, 0 );
82 $this->timeout =
$params[
'timeout'];
84 if ( !isset(
$params[
'cache'] ) ) {
87 $this->srvCache =
$params[
'cache'];
92 $this->logger =
new Psr\Log\NullLogger();
94 'connTimeout' => $this->timeout,
95 'reqTimeout' => $this->timeout,
96 'logger' => $this->logger
102 $this->
http->setLogger( $logger );
108 return array_key_exists(
$name, $this->procCache[
'config'] );
114 if ( !array_key_exists(
$name, $this->procCache[
'config'] ) ) {
118 return $this->procCache[
'config'][
$name];
125 if ( $this->procCache !==
null ) {
129 $now = microtime(
true );
130 $key = $this->srvCache->makeGlobalKey(
139 $loop =
new WaitConditionLoop(
140 function ()
use ( $key, $now, &$data, &$error ) {
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." );
146 return WaitConditionLoop::CONDITION_REACHED;
151 if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
154 if ( is_array( $config ) ) {
159 $data = [
'config' => $config,
'expires' => $expiry ];
162 $this->logger->info(
"Refreshed stale etcd configuration cache." );
164 return WaitConditionLoop::CONDITION_REACHED;
166 $this->logger->error(
"Failed to fetch configuration: $error" );
169 return WaitConditionLoop::CONDITION_FAILED;
173 $this->srvCache->unlock( $key );
177 if ( is_array( $data ) ) {
178 $this->logger->info(
"Using stale etcd configuration cache." );
180 return WaitConditionLoop::CONDITION_REACHED;
183 return WaitConditionLoop::CONDITION_CONTINUE;
188 if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) {
190 throw new ConfigException(
"Failed to load configuration from etcd: $error" );
193 $this->procCache = $data;
201 $servers = $dsd->getServers();
208 $server = $dsd->pickServer( $servers );
212 if ( is_array( $config ) || !$retry ) {
217 $servers = $dsd->removeServer( $server, $servers );
218 }
while ( $servers );
220 return [ $config, $error, $retry ];
229 list( $rcode, $rdesc, , $rbody, $rerr ) = $this->
http->run( [
231 'url' =>
"{$this->protocol}://{$address}/v2/keys/{$this->directory}/?recursive=true",
232 'headers' => [
'content-type' =>
'application/json' ]
235 static $terminalCodes = [ 404 =>
true ];
236 if ( $rcode < 200 || $rcode > 399 ) {
239 strlen( $rerr ) ? $rerr :
"HTTP $rcode ($rdesc)",
240 empty( $terminalCodes[$rcode] )
246 return [
null,
$e->getMessage(),
false ];
257 $info = json_decode( $rbody,
true );
258 if ( $info ===
null ) {
261 if ( !isset( $info[
'node'] ) || !is_array( $info[
'node'] ) ) {
263 "Unexpected JSON response: Missing or invalid node at top level." );
279 if ( !isset( $dirNode[
'nodes'] ) ) {
281 "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
283 if ( !is_array( $dirNode[
'nodes'] ) ) {
285 "Unexpected JSON response in dir '$dirName'; 'nodes' is not an array." );
288 foreach ( $dirNode[
'nodes']
as $node ) {
289 $baseName = basename( $node[
'key'] );
290 $fullName = $dirName ===
'' ? $baseName :
"$dirName/$baseName";
291 if ( !empty( $node[
'dir'] ) ) {
295 if ( !is_array(
$value ) || !array_key_exists(
'val',
$value ) ) {
299 $config[$fullName] =
$value[
'val'];
309 if ( $this->encoding ===
'YAML' ) {
310 return yaml_parse( $string );
312 return json_decode( $string,
true );