24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerInterface;
26 use Wikimedia\WaitConditionLoop;
33 class EtcdConfig implements Config, LoggerAwareInterface {
82 $this->protocol =
$params[
'protocol'];
84 $this->directoryHash = sha1( $this->
directory );
85 $this->encoding =
$params[
'encoding'];
86 $this->skewCacheTTL =
$params[
'skewTTL'];
87 $this->baseCacheTTL = max(
$params[
'cacheTTL'] - $this->skewCacheTTL, 0 );
88 $this->timeout =
$params[
'timeout'];
90 if ( !isset(
$params[
'cache'] ) ) {
93 $this->srvCache =
$params[
'cache'];
98 $this->logger =
new Psr\Log\NullLogger();
100 'connTimeout' => $this->timeout,
101 'reqTimeout' => $this->timeout
112 return array_key_exists(
$name, $this->procCache[
'config'] );
118 if ( !array_key_exists(
$name, $this->procCache[
'config'] ) ) {
122 return $this->procCache[
'config'][
$name];
126 if ( $this->procCache !==
null ) {
130 $now = microtime(
true );
131 $key = $this->srvCache->makeKey(
'variable', $this->directoryHash );
136 $loop =
new WaitConditionLoop(
137 function ()
use ( $key, $now, &$data, &$error ) {
139 $data = $this->srvCache->get( $key );
140 if ( is_array( $data ) && $data[
'expires'] > $now ) {
141 $this->logger->debug(
"Found up-to-date etcd configuration cache." );
143 return WaitConditionLoop::CONDITION_REACHED;
148 if ( $this->srvCache->lock( $key, 0, $this->baseCacheTTL ) ) {
151 if ( $config ===
null ) {
152 $this->logger->error(
"Failed to fetch configuration: $error" );
155 ? WaitConditionLoop::CONDITION_CONTINUE
156 : WaitConditionLoop::CONDITION_FAILED;
163 $data = [
'config' => $config,
'expires' => $expiry ];
166 $this->logger->info(
"Refreshed stale etcd configuration cache." );
168 return WaitConditionLoop::CONDITION_REACHED;
170 $this->srvCache->unlock( $key );
174 if ( is_array( $data ) ) {
175 $this->logger->info(
"Using stale etcd configuration cache." );
177 return WaitConditionLoop::CONDITION_REACHED;
180 return WaitConditionLoop::CONDITION_CONTINUE;
185 if ( $loop->invoke() !== WaitConditionLoop::CONDITION_REACHED ) {
187 throw new ConfigException(
"Failed to load configuration from etcd: $error" );
190 $this->procCache = $data;
198 $servers = $dsd->getServers();
205 $server = $dsd->pickServer( $servers );
209 if ( is_array( $config ) || !$retry ) {
214 $dsd->removeServer( $server, $servers );
215 }
while ( $servers );
217 return [ $config, $error, $retry ];
226 list( $rcode, $rdesc, , $rbody, $rerr ) = $this->
http->run( [
228 'url' =>
"{$this->protocol}://{$address}/v2/keys/{$this->directory}/",
229 'headers' => [
'content-type' =>
'application/json' ]
232 static $terminalCodes = [ 404 =>
true ];
233 if ( $rcode < 200 || $rcode > 399 ) {
236 strlen( $rerr ) ? $rerr :
"HTTP $rcode ($rdesc)",
237 empty( $terminalCodes[$rcode] )
241 $info = json_decode( $rbody,
true );
242 if ( $info ===
null || !isset( $info[
'node'][
'nodes'] ) ) {
243 return [
null, $rcode,
"Unexpected JSON response; missing 'nodes' list.",
false ];
247 foreach ( $info[
'node'][
'nodes']
as $node ) {
248 if ( !empty( $node[
'dir'] ) ) {
252 $name = basename( $node[
'key'] );
255 return [
null,
"Failed to parse value for '$name'.",
false ];
261 return [ $config,
null,
false ];
269 if ( $this->encoding ===
'YAML' ) {
270 return yaml_parse( $string );
272 return json_decode( $string,
true );