org.apache.nifi.controller.state.providers.zookeeper.ZooKeeperStateProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.controller.state.providers.zookeeper.ZooKeeperStateProvider.java

Source

/*
 * 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);
    }
}