org.apache.flink.runtime.resourcemanager.autoscale.hotupdate.ZooKeeperHotUpdateConfigRetrievalService Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.flink.runtime.resourcemanager.autoscale.hotupdate;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.resourcemanager.autoscale.utils.HotUpdateConfiguration;
import org.apache.flink.runtime.util.ZooKeeperUtils;
import org.apache.flink.util.FlinkException;
import org.apache.flink.util.Preconditions;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.UnhandledErrorListener;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This implementation of the {@link HotUpdateConfigRetrievalService} retrieves the hot-update configuration which has
* been committed by user.
* The new configuration is retrieved from ZooKeeper.
*/
public class ZooKeeperHotUpdateConfigRetrievalService implements HotUpdateConfigRetrievalService, NodeCacheListener, UnhandledErrorListener {
private static final Logger LOG = LoggerFactory.getLogger(
ZooKeeperHotUpdateConfigRetrievalService.class);
private final Object lock = new Object();
private static final Pattern CONFIG_PATTERN =
Pattern.compile("([a-z0-9\\.-]+)=([a-z0-9\\.-]+)");
/** Connection to the used ZooKeeper quorum. */
private final CuratorFramework client;
/** Curator recipe to watch changes of a specific ZooKeeper node. */
private final NodeCache cache;
/** Path of the znode which contains the hot-update configuration. */
private final String retrievalPath;
private Map lastVersion;
/** Listener which will be notified about config changes. */
private HotUpdateConfigRetrievalListener configUpdateListener;
private final ConnectionStateListener connectionStateListener = new ConnectionStateListener() {
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
handleStateChange(newState);
}
};
/**
* Creates a testing {@link ZooKeeperHotUpdateConfigRetrievalService}.
*
* @param client Tesing client, should be closed proactively.
* @param cache Tesing node cache.
* @param retrievalPath of the test case.
*/
@VisibleForTesting
public ZooKeeperHotUpdateConfigRetrievalService(
CuratorFramework client,
NodeCache cache,
String retrievalPath) {
this.client = client;
this.cache = cache;
this.retrievalPath = Preconditions.checkNotNull(retrievalPath);
this.configUpdateListener = null;
this.lastVersion = new HashMap<>();
running = true;
}
/**
* Creates a configuration retrieval service which uses ZooKeeper to retrieve the new configuration.
*
* @param configuration Configuration to create CuratorFramework
* @param retrievalPath Path of the ZooKeeper node which contains the leader information
*/
public ZooKeeperHotUpdateConfigRetrievalService(Configuration configuration, String retrievalPath) {
this.client = ZooKeeperUtils.startCuratorFramework(configuration);
this.cache = new NodeCache(client, retrievalPath);
this.retrievalPath = Preconditions.checkNotNull(retrievalPath);
this.configUpdateListener = null;
this.lastVersion = new HashMap<>();
running = false;
}
@Override
public void start(HotUpdateConfigRetrievalListener listener) throws Exception {
Preconditions.checkNotNull(listener, "Listener must not be null.");
Preconditions.checkState(configUpdateListener == null, "ZooKeeperHotUpdateConfigRetrievalService can " +
"only be started once.");
LOG.info("Starting ZooKeeperHotUpdateConfigRetrievalService {}.", retrievalPath);
configUpdateListener = listener;
synchronized (lock) {
client.getUnhandledErrorListenable().addListener(this);
cache.getListenable().addListener(this);
cache.start();
ChildData childData = cache.getCurrentData();
this.lastVersion = readConfiguration(childData);
client.getConnectionStateListenable().addListener(connectionStateListener);
running = true;
}
}
@Override
public void stop() throws Exception {
LOG.info("Stopping ZooKeeperLeaderRetrievalService {}.", retrievalPath);
synchronized (lock) {
if (!running) {
return;
}
running = false;
}
client.getUnhandledErrorListenable().removeListener(this);
client.getConnectionStateListenable().removeListener(connectionStateListener);
try {
cache.close();
} catch (IOException e) {
LOG.error("Could not properly stop the ZooKeeperLeaderRetrievalService.", e);
}
try {
ZooKeeperUtils.cleanupZooKeeperPaths(client);
} catch (Exception exception) {
LOG.error("Could not properly clean up ZooKeeperPaths.", exception);
}
client.close();
}
protected void handleStateChange(ConnectionState newState) {
switch (newState) {
case CONNECTED:
LOG.debug("Connected to ZooKeeper quorum. Hot update configuration retrieval can start.");
break;
case SUSPENDED:
LOG.warn("Connection to ZooKeeper suspended. Can no longer retrieve new configuration from " +
"ZooKeeper.");
break;
case RECONNECTED:
LOG.info("Connection to ZooKeeper was reconnected. New configuration can be retrieve again.");
break;
case LOST:
LOG.warn("Connection to ZooKeeper lost. Can no longer retrieve new configuration from " +
"ZooKeeper.");
break;
default:
LOG.error("Unknown ConnectionState {} from ZooKeeper.", newState);
}
}
private boolean running;
@Override
public void unhandledError(String s, Throwable throwable) {
configUpdateListener.handleError(new FlinkException("Unhandled error in ZooKeeperHotUpdateConfigRetrievalService:" + s, throwable));
}
@Override
public void nodeChanged() throws Exception {
synchronized (lock) {
if (running) {
try {
LOG.info("Configuration has changed.");
ChildData childData = cache.getCurrentData();
Map currentVersion = readConfiguration(childData);
Map updatedConfig = new HashMap<>();
Configuration configuration = new Configuration();
for (Map.Entry config : currentVersion.entrySet()) {
if (!lastVersion.containsKey(config.getKey())
|| !lastVersion.get(config.getKey()).equals(config.getValue())) {
updatedConfig.put(config.getKey(), config.getValue());
}
}
if (!updatedConfig.isEmpty()) {
configuration.addAll(updatedConfig);
LOG.info("Detect configMap modified, updated config, last version {} current version {}.", lastVersion, currentVersion);
configUpdateListener.notifyUpdateConfig(HotUpdateConfiguration.fromConfiguration(configuration));
}
lastVersion = currentVersion;
} catch (Exception e) {
configUpdateListener.handleError(new Exception("Could not handle node changed event.", e));
throw e;
}
}
}
}
@VisibleForTesting
public Map readConfiguration(@Nullable ChildData childData) {
Map config = new HashMap<>();
if (null == childData) {
return config;
}
byte[] data = childData.getData();
if (null == data || data.length == 0) {
return config;
}
String dataString = new String(data);
for (String configLine: dataString.split("\n")) {
Matcher configMatcher = CONFIG_PATTERN.matcher(configLine);
if (configMatcher.matches()) {
config.put(configMatcher.group(1), configMatcher.group(2));
} else {
LOG.warn("Could not resolve config line {}.", configLine);
}
}
return config;
}
}