21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerInterface;
23 use Wikimedia\ObjectFactory;
24 use Wikimedia\WaitConditionLoop;
77 $this->host = $params[
'host'];
78 $this->protocol = $params[
'protocol'];
79 $this->directory = trim( $params[
'directory'],
'/' );
80 $this->encoding = $params[
'encoding'];
81 $this->skewCacheTTL = $params[
'skewTTL'];
82 $this->baseCacheTTL = max( $params[
'cacheTTL'] - $this->skewCacheTTL, 0 );
83 $this->timeout = $params[
'timeout'];
85 if ( !isset( $params[
'cache'] ) ) {
87 } elseif ( $params[
'cache'] instanceof
BagOStuff ) {
88 $this->srvCache = $params[
'cache'];
90 $this->srvCache = ObjectFactory::getObjectFromSpec( $params[
'cache'] );
93 $this->logger =
new Psr\Log\NullLogger();
95 'connTimeout' => $this->timeout,
96 'reqTimeout' => $this->timeout,
97 'logger' => $this->logger
103 $this->http->setLogger(
$logger );
106 public function has( $name ) {
109 return array_key_exists( $name, $this->procCache[
'config'] );
112 public function get( $name ) {
115 if ( !array_key_exists( $name, $this->procCache[
'config'] ) ) {
119 return $this->procCache[
'config'][$name];
124 return $this->procCache[
'modifiedIndex'];
131 if ( $this->procCache !==
null ) {
135 $now = microtime(
true );
136 $key = $this->srvCache->makeGlobalKey(
145 $loop =
new WaitConditionLoop(
146 function () use ( $key, $now, &$data, &$error ) {
148 $data = $this->srvCache->get( $key );
149 if ( is_array( $data ) && $data[
'expires'] > $now ) {
150 $this->logger->debug(
"Found up-to-date etcd configuration cache." );
152 return WaitConditionLoop::CONDITION_REACHED;
157 if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
160 $error = $etcdResponse[
'error'];
161 if ( is_array( $etcdResponse[
'config'] ) ) {
167 'config' => $etcdResponse[
'config'],
168 'expires' => $expiry,
169 'modifiedIndex' => $etcdResponse[
'modifiedIndex']
173 $this->logger->info(
"Refreshed stale etcd configuration cache." );
175 return WaitConditionLoop::CONDITION_REACHED;
177 $this->logger->error(
"Failed to fetch configuration: $error" );
178 if ( !$etcdResponse[
'retry'] ) {
180 return WaitConditionLoop::CONDITION_FAILED;
184 $this->srvCache->unlock( $key );
188 if ( is_array( $data ) ) {
189 $this->logger->info(
"Using stale etcd configuration cache." );
191 return WaitConditionLoop::CONDITION_REACHED;
194 return WaitConditionLoop::CONDITION_CONTINUE;
199 if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) {
201 throw new ConfigException(
"Failed to load configuration from etcd: $error" );
204 $this->procCache = $data;
213 $servers = $dsd->getServers();
220 $server = $dsd->pickServer( $servers );
229 $servers = $dsd->removeServer( $server, $servers );
230 }
while ( $servers );
241 list( $rcode, $rdesc, , $rbody, $rerr ) = $this->http->run( [
243 'url' =>
"{$this->protocol}://{$address}/v2/keys/{$this->directory}/?recursive=true",
244 'headers' => [
'content-type' =>
'application/json' ]
247 $response = [
'config' =>
null,
'error' =>
null,
'retry' =>
false,
'modifiedIndex' => 0 ];
249 static $terminalCodes = [ 404 =>
true ];
250 if ( $rcode < 200 || $rcode > 399 ) {
251 $response[
'error'] = strlen( $rerr ) ? $rerr :
"HTTP $rcode ($rdesc)";
252 $response[
'retry'] = empty( $terminalCodes[$rcode] );
259 $parsedResponse = [
'error' => $e->getMessage() ];
261 return array_merge(
$response, $parsedResponse );
271 $info = json_decode( $rbody,
true );
272 if ( $info ===
null ) {
275 if ( !isset( $info[
'node'] ) || !is_array( $info[
'node'] ) ) {
277 "Unexpected JSON response: Missing or invalid node at top level." );
280 $lastModifiedIndex = $this->
parseDirectory(
'', $info[
'node'], $config );
281 return [
'modifiedIndex' => $lastModifiedIndex,
'config' => $config ];
294 $lastModifiedIndex = 0;
295 if ( !isset( $dirNode[
'nodes'] ) ) {
297 "Unexpected JSON response in dir '$dirName'; missing 'nodes' list." );
299 if ( !is_array( $dirNode[
'nodes'] ) ) {
301 "Unexpected JSON response in dir '$dirName'; 'nodes' is not an array." );
304 foreach ( $dirNode[
'nodes'] as $node ) {
305 $baseName = basename( $node[
'key'] );
306 $fullName = $dirName ===
'' ? $baseName :
"$dirName/$baseName";
307 if ( !empty( $node[
'dir'] ) ) {
308 $lastModifiedIndex = max(
310 $lastModifiedIndex );
313 if ( !is_array( $value ) || !array_key_exists(
'val', $value ) ) {
316 $lastModifiedIndex = max( $node[
'modifiedIndex'], $lastModifiedIndex );
317 $config[$fullName] = $value[
'val'];
320 return $lastModifiedIndex;
328 if ( $this->encoding ===
'YAML' ) {
329 return yaml_parse( $string );
331 return json_decode( $string,
true );