/* * 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.nifi.controller.state.providers.zookeeper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.Validator; import org.apache.nifi.components.state.Scope; import org.apache.nifi.components.state.StateMap; import org.apache.nifi.components.state.StateProviderInitializationContext; import org.apache.nifi.components.state.exception.StateTooLargeException; import org.apache.nifi.controller.state.StandardStateMap; import org.apache.nifi.controller.state.providers.AbstractStateProvider; import org.apache.nifi.processor.util.StandardValidators; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.KeeperException.Code; import org.apache.zookeeper.KeeperException.NoNodeException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZKUtil; import org.apache.zookeeper.ZooDefs.Ids; import org.apache.zookeeper.ZooKeeper; import org.apache.zookeeper.client.ConnectStringParser; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Stat; /** * ZooKeeperStateProvider utilizes a ZooKeeper based store, whether provided internally via configuration and enabling of the {@link org.apache.nifi.controller.state.server.ZooKeeperStateServer} * or through an externally configured location. This implementation caters to a clustered NiFi environment and accordingly only provides {@link Scope#CLUSTER} scoping to enforce * consistency across configuration interactions. */ public class ZooKeeperStateProvider extends AbstractStateProvider { private static final int ONE_MB = 1024 * 1024; static final AllowableValue OPEN_TO_WORLD = new AllowableValue("Open", "Open", "ZNodes will be open to any ZooKeeper client."); static final AllowableValue CREATOR_ONLY = new AllowableValue("CreatorOnly", "CreatorOnly", "ZNodes will be accessible only by the creator. The creator will have full access to create, read, write, delete, and administer the ZNodes."); static final PropertyDescriptor CONNECTION_STRING = new PropertyDescriptor.Builder() .name("Connect String") .description("The ZooKeeper Connect String to use. This is a comma-separated list of hostname/IP and port tuples, such as \"host1:2181,host2:2181,127.0.0.1:2181\". If a port is not " + "specified it defaults to the ZooKeeper client port default of 2181") .addValidator(new Validator() { @Override public ValidationResult validate(String subject, String input, ValidationContext context) { final String connectionString = context.getProperty(CONNECTION_STRING).getValue(); try { new ConnectStringParser(connectionString); } catch (Exception e) { return new ValidationResult.Builder().subject(subject).input(input).explanation("Invalid Connect String: " + connectionString).valid(false).build(); } return new ValidationResult.Builder().subject(subject).input(input).explanation("Valid Connect String").valid(true).build(); } }) .required(false) .build(); static final PropertyDescriptor SESSION_TIMEOUT = new PropertyDescriptor.Builder() .name("Session Timeout") .description("Specifies how long this instance of NiFi is allowed to be disconnected from ZooKeeper before creating a new ZooKeeper Session") .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) .defaultValue("30 sec") .required(true) .build(); static final PropertyDescriptor ROOT_NODE = new PropertyDescriptor.Builder() .name("Root Node") .description("The Root Node to use in ZooKeeper to store state in") .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .defaultValue("/nifi") .required(true) .build(); static final PropertyDescriptor ACCESS_CONTROL = new PropertyDescriptor.Builder() .name("Access Control") .description("Specifies the Access Controls that will be placed on ZooKeeper ZNodes that are created by this State Provider") .allowableValues(OPEN_TO_WORLD, CREATOR_ONLY) .defaultValue(OPEN_TO_WORLD.getValue()) .required(true) .build(); private static final byte ENCODING_VERSION = 1; private ZooKeeper zooKeeper; // effectively final private int timeoutMillis; private String rootNode; private String connectionString; private byte[] auth; private List<ACL> acl; public ZooKeeperStateProvider() { } @Override public List<PropertyDescriptor> getSupportedPropertyDescriptors() { final List<PropertyDescriptor> properties = new ArrayList<>(); properties.add(CONNECTION_STRING); properties.add(SESSION_TIMEOUT); properties.add(ROOT_NODE); properties.add(ACCESS_CONTROL); return properties; } @Override public synchronized void init(final StateProviderInitializationContext context) { connectionString = context.getProperty(CONNECTION_STRING).getValue(); rootNode = context.getProperty(ROOT_NODE).getValue(); timeoutMillis = context.getProperty(SESSION_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS).intValue(); if (context.getProperty(ACCESS_CONTROL).getValue().equalsIgnoreCase(CREATOR_ONLY.getValue())) { acl = Ids.CREATOR_ALL_ACL; } else { acl = Ids.OPEN_ACL_UNSAFE; } } @Override public synchronized void shutdown() { if (zooKeeper != null) { try { zooKeeper.close(); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } } zooKeeper = null; } // visible for testing synchronized ZooKeeper getZooKeeper() throws IOException { if (zooKeeper != null && !zooKeeper.getState().isAlive()) { invalidateClient(); } if (zooKeeper == null) { zooKeeper = new ZooKeeper(connectionString, timeoutMillis, new Watcher() { @Override public void process(WatchedEvent event) { } }); if (auth != null) { zooKeeper.addAuthInfo("digest", auth); } } return zooKeeper; } private synchronized void invalidateClient() { shutdown(); } private String getComponentPath(final String componentId) { return rootNode + "/components/" + componentId; } private void verifyEnabled() throws IOException { if (!isEnabled()) { throw new IOException("Cannot update or retrieve cluster state because node is no longer connected to a cluster."); } } @Override public void onComponentRemoved(final String componentId) throws IOException { try { ZKUtil.deleteRecursive(getZooKeeper(), getComponentPath(componentId)); } catch (final KeeperException ke) { // Node doesn't exist so just ignore final Code exceptionCode = ke.code(); if (Code.NONODE == exceptionCode) { return; } if (Code.SESSIONEXPIRED == exceptionCode) { invalidateClient(); onComponentRemoved(componentId); return; } throw new IOException("Unable to remove state for component with ID '" + componentId + " with exception code " + exceptionCode, ke); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Failed to remove state for component with ID '" + componentId + "' from ZooKeeper due to being interrupted", e); } } @Override public Scope[] getSupportedScopes() { return new Scope[]{Scope.CLUSTER}; } @Override public void setState(final Map<String, String> state, final String componentId) throws IOException { setState(state, -1, componentId); } private byte[] serialize(final Map<String, String> stateValues) throws IOException { try (final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream(baos)) { dos.writeByte(ENCODING_VERSION); dos.writeInt(stateValues.size()); for (final Map.Entry<String, String> entry : stateValues.entrySet()) { final boolean hasKey = entry.getKey() != null; final boolean hasValue = entry.getValue() != null; dos.writeBoolean(hasKey); if (hasKey) { dos.writeUTF(entry.getKey()); } dos.writeBoolean(hasValue); if (hasValue) { dos.writeUTF(entry.getValue()); } } return baos.toByteArray(); } } private StateMap deserialize(final byte[] data, final int recordVersion, final String componentId) throws IOException { try (final ByteArrayInputStream bais = new ByteArrayInputStream(data); final DataInputStream dis = new DataInputStream(bais)) { final byte encodingVersion = dis.readByte(); if (encodingVersion > ENCODING_VERSION) { throw new IOException("Retrieved a response from ZooKeeper when retrieving state for component with ID " + componentId + ", but the response was encoded using the ZooKeeperStateProvider Encoding Version of " + encodingVersion + " but this instance can only decode versions up to " + ENCODING_VERSION + "; it appears that the state was encoded using a newer version of NiFi than is currently running. This information cannot be decoded."); } final int numEntries = dis.readInt(); final Map<String, String> stateValues = new HashMap<>(numEntries); for (int i = 0; i < numEntries; i++) { final boolean hasKey = dis.readBoolean(); final String key = hasKey ? dis.readUTF() : null; final boolean hasValue = dis.readBoolean(); final String value = hasValue ? dis.readUTF() : null; stateValues.put(key, value); } return new StandardStateMap(stateValues, recordVersion); } } private void setState(final Map<String, String> stateValues, final int version, final String componentId) throws IOException { try { setState(stateValues, version, componentId, true); } catch (final NoNodeException nne) { // should never happen because we are passing 'true' for allowNodeCreation throw new IOException("Unable to create Node in ZooKeeper to set state for component with ID " + componentId, nne); } } /** * Sets the component state to the given stateValues if and only if the version is equal to the version currently * tracked by ZooKeeper (or if the version is -1, in which case the state will be updated regardless of the version). * * @param stateValues the new values to set * @param version the expected version of the ZNode * @param componentId the ID of the component whose state is being updated * @param allowNodeCreation if <code>true</code> and the corresponding ZNode does not exist in ZooKeeper, it will be created; if <code>false</code> * and the corresponding node does not exist in ZooKeeper, a {@link KeeperException.NoNodeException} will be thrown * * @throws IOException if unable to communicate with ZooKeeper * @throws NoNodeException if the corresponding ZNode does not exist in ZooKeeper and allowNodeCreation is set to <code>false</code> * @throws StateTooLargeException if the state to be stored exceeds the maximum size allowed by ZooKeeper (1 MB, after serialization) */ private void setState(final Map<String, String> stateValues, final int version, final String componentId, final boolean allowNodeCreation) throws IOException, NoNodeException { verifyEnabled(); try { final String path = getComponentPath(componentId); final byte[] data = serialize(stateValues); if (data.length > ONE_MB) { throw new StateTooLargeException("Failed to set cluster-wide state in ZooKeeper for component with ID " + componentId + " because the state had " + stateValues.size() + " values, which serialized to " + data.length + " bytes, and the maximum allowed by ZooKeeper is 1 MB (" + ONE_MB + " bytes)"); } final ZooKeeper keeper = getZooKeeper(); try { keeper.setData(path, data, version); } catch (final NoNodeException nne) { if (allowNodeCreation) { createNode(path, data, componentId, stateValues, acl); return; } else { throw nne; } } } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Failed to set cluster-wide state in ZooKeeper for component with ID " + componentId + " due to interruption", e); } catch (final NoNodeException nne) { throw nne; } catch (final KeeperException ke) { if (Code.SESSIONEXPIRED == ke.code()) { invalidateClient(); setState(stateValues, version, componentId, allowNodeCreation); return; } if (Code.NODEEXISTS == ke.code()) { setState(stateValues, version, componentId, allowNodeCreation); return; } throw new IOException("Failed to set cluster-wide state in ZooKeeper for component with ID " + componentId, ke); } catch (final StateTooLargeException stle) { throw stle; } catch (final IOException ioe) { throw new IOException("Failed to set cluster-wide state in ZooKeeper for component with ID " + componentId, ioe); } } private void createNode(final String path, final byte[] data, final String componentId, final Map<String, String> stateValues, final List<ACL> acls) throws IOException, KeeperException { try { if (data != null && data.length > ONE_MB) { throw new StateTooLargeException("Failed to set cluster-wide state in ZooKeeper for component with ID " + componentId + " because the state had " + stateValues.size() + " values, which serialized to " + data.length + " bytes, and the maximum allowed by ZooKeeper is 1 MB (" + ONE_MB + " bytes)"); } getZooKeeper().create(path, data, acls, CreateMode.PERSISTENT); } catch (final InterruptedException ie) { throw new IOException("Failed to update cluster-wide state due to interruption", ie); } catch (final KeeperException ke) { final Code exceptionCode = ke.code(); if (Code.NONODE == exceptionCode) { final String parentPath = StringUtils.substringBeforeLast(path, "/"); createNode(parentPath, null, componentId, stateValues, Ids.OPEN_ACL_UNSAFE); createNode(path, data, componentId, stateValues, acls); return; } if (Code.SESSIONEXPIRED == exceptionCode) { invalidateClient(); createNode(path, data, componentId, stateValues, acls); return; } // Node already exists. Node must have been created by "someone else". Just set the data. if (Code.NODEEXISTS == exceptionCode) { try { getZooKeeper().setData(path, data, -1); return; } catch (final KeeperException ke1) { // Node no longer exists -- it was removed by someone else. Go recreate the node. if (ke1.code() == Code.NONODE) { createNode(path, data, componentId, stateValues, acls); return; } } catch (final InterruptedException ie) { throw new IOException("Failed to update cluster-wide state due to interruption", ie); } } throw ke; } } @Override public StateMap getState(final String componentId) throws IOException { verifyEnabled(); try { final Stat stat = new Stat(); final String path = getComponentPath(componentId); final byte[] data = getZooKeeper().getData(path, false, stat); final StateMap stateMap = deserialize(data, stat.getVersion(), componentId); return stateMap; } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Failed to obtain value from ZooKeeper for component with ID " + componentId + ", due to interruption", e); } catch (final KeeperException ke) { final Code exceptionCode = ke.code(); if (Code.NONODE == exceptionCode) { return new StandardStateMap(null, -1L); } if (Code.SESSIONEXPIRED == exceptionCode) { invalidateClient(); return getState(componentId); } throw new IOException("Failed to obtain value from ZooKeeper for component with ID " + componentId + " with exception code " + exceptionCode, ke); } catch (final IOException ioe) { // provide more context in the error message throw new IOException("Failed to obtain value from ZooKeeper for component with ID " + componentId, ioe); } } @Override public boolean replace(final StateMap oldValue, final Map<String, String> newValue, final String componentId) throws IOException { verifyEnabled(); try { setState(newValue, (int) oldValue.getVersion(), componentId, false); return true; } catch (final NoNodeException nne) { return false; } catch (final IOException ioe) { final Throwable cause = ioe.getCause(); if (cause != null && cause instanceof KeeperException) { final KeeperException ke = (KeeperException) cause; if (Code.BADVERSION == ke.code()) { return false; } } throw ioe; } } @Override public void clear(final String componentId) throws IOException { verifyEnabled(); setState(Collections.<String, String>emptyMap(), componentId); } }