NIFI-10975 Add Kubernetes Leader Election and State Provider (#6779)

* NIFI-10975 Added Kubernetes Leader Election and State Provider
- Added Kubernetes Leader Election Manager based on Kubernetes Leases
- Added Kubernetes State Provider based on Kubernetes ConfigMaps
- Added nifi-kubernetes-client for generalized access to Fabric8 Kubernetes Client
- Added nifi.cluster.leader.election.implementation Property defaulting to CuratorLeaderElectionManager
- Refactored LeaderElectionManager to nifi-framework-api for Extension Discovering Manager
- Refactored shared ZooKeeper configuration to nifi-framework-cluster-zookeeper

* NIFI-10975 Updated Kubernetes Client and StateMap
- Upgraded Kubernetes Client from 6.2.0 to 6.3.0
- Added getStateVersion to StateMap and deprecated getVersion
- Updated Docker start.sh with additional properties

* NIFI-10975 Corrected MockStateManager.assertStateSet()
* NIFI-10975 Upgraded Kubernetes Client from 6.3.0 to 6.3.1
* NIFI-10975 Corrected unregister leader and disabled release on cancel

* NIFI-10975 Corrected findLeader handling of Lease expiration
- Changed LeaderElectionManager.getLeader() return to Optional String

* NIFI-10975 Corrected StandardNiFiServiceFacade handling of Optional Leader
* NIFI-10975 Changed getLeader() to call findLeader() to avoid stale cached values
* NIFI-10975 Updated LeaderElectionCommand to run LeaderElector in loop
* NIFI-10975 Rebased on project version 2.0.0-SNAPSHOT

* NIFI-10975 Corrected Gson and AspectJ versions
- Updated versions to match current main branch and avoid reverting
This commit is contained in:
exceptionfactory 2023-03-07 13:14:24 -06:00 committed by GitHub
parent d3908dede8
commit 512155ba2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2950 additions and 1764 deletions

View File

@ -18,6 +18,7 @@
package org.apache.nifi.components.state;
import java.util.Map;
import java.util.Optional;
/**
* Provides a representation of a component's state at some point in time.
@ -29,13 +30,27 @@ public interface StateMap {
* Though this number is monotonically increasing, it should not be expected to increment always
* from X to X+1. I.e., version numbers may be skipped.
*
* @deprecated This method should be replaced with getStateVersion()
*
* @return the version associated with the state
*/
@Deprecated
long getVersion();
/**
* Returns the value associated with the given key
* Get state version is not guaranteed to be numeric, but can be used to compare against an expected version.
* The default implementation uses the available version number and considers -1 as indicating an empty version
*
* @return State version or empty when not known
*/
default Optional<String> getStateVersion() {
final long version = getVersion();
return version == -1 ? Optional.empty() : Optional.of(String.valueOf(version));
}
/**
* Returns the value associated with the given key
*n
* @param key the key whose value should be retrieved
* @return the value associated with the given key, or <code>null</code> if no value is associated
* with this key.

View File

@ -919,6 +919,12 @@ language governing permissions and limitations under the License. -->
<version>2.0.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-nar</artifactId>
<version>2.0.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<!-- AspectJ library needed by the Java Agent used for native library loading (see bootstrap.conf) -->
<dependency>
<groupId>org.aspectj</groupId>

View File

@ -0,0 +1,36 @@
<?xml version="1.0"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-commons</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-kubernetes-client</artifactId>
<description>Minimal abstraction for access Kubernetes REST API Resources</description>
<dependencies>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client-api</artifactId>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,31 @@
/*
* 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.kubernetes.client;
import io.fabric8.kubernetes.client.KubernetesClient;
/**
* Abstraction for providing a configured Kubernetes Client
*/
public interface KubernetesClientProvider {
/**
* Get configured Kubernetes Client
*
* @return Kubernetes Client
*/
KubernetesClient getKubernetesClient();
}

View File

@ -0,0 +1,29 @@
/*
* 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.kubernetes.client;
/**
* Kubernetes Namespace Provider
*/
public interface NamespaceProvider {
/**
* Get Namespace
*
* @return Kubernetes Namespace
*/
String getNamespace();
}

View File

@ -0,0 +1,53 @@
/*
* 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.kubernetes.client;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Service Account Namespace Provider based on standard file location
*/
public class ServiceAccountNamespaceProvider implements NamespaceProvider {
protected static final String NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
protected static final String DEFAULT_NAMESPACE = "default";
/**
* Get Namespace from Service Account location or return default namespace when not found
*
* @return Kubernetes Namespace
*/
@Override
public String getNamespace() {
final Path namespacePath = Paths.get(NAMESPACE_PATH);
return Files.isReadable(namespacePath) ? getNamespace(namespacePath) : DEFAULT_NAMESPACE;
}
private String getNamespace(final Path namespacePath) {
try {
final byte[] bytes = Files.readAllBytes(namespacePath);
return new String(bytes, StandardCharsets.UTF_8).trim();
} catch (final IOException e) {
throw new UncheckedIOException("Read Service Account namespace failed", e);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.kubernetes.client;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
/**
* Standard implementation of Kubernetes Client Provider with default properties
*/
public class StandardKubernetesClientProvider implements KubernetesClientProvider {
private volatile KubernetesClient kubernetesClient;
/**
* Get Kubernetes Client with default configuration discovery
*
* @return Kubernetes Client
*/
@Override
public KubernetesClient getKubernetesClient() {
if (kubernetesClient == null) {
kubernetesClient = buildKubernetesClient();
}
return kubernetesClient;
}
private KubernetesClient buildKubernetesClient() {
return new KubernetesClientBuilder().build();
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.kubernetes.client;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class ServiceAccountNamespaceProviderTest {
ServiceAccountNamespaceProvider provider;
@BeforeEach
void setProvider() {
provider = new ServiceAccountNamespaceProvider();
}
@Test
void testGetNamespace() {
final String namespace = provider.getNamespace();
final Path namespacePath = Paths.get(ServiceAccountNamespaceProvider.NAMESPACE_PATH);
if (Files.isReadable(namespacePath)) {
assertNotNull(namespace);
} else {
assertEquals(ServiceAccountNamespaceProvider.DEFAULT_NAMESPACE, namespace);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.kubernetes.client;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class StandardKubernetesClientProviderTest {
StandardKubernetesClientProvider provider;
@BeforeEach
void setProvider() {
provider = new StandardKubernetesClientProvider();
}
@Timeout(5)
@Test
void testGetKubernetesClient() {
final KubernetesClient kubernetesClient = provider.getKubernetesClient();
assertNotNull(kubernetesClient);
}
}

View File

@ -252,6 +252,7 @@ public class NiFiProperties extends ApplicationProperties {
public static final String CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL = "nifi.cluster.protocol.heartbeat.interval";
public static final String CLUSTER_PROTOCOL_HEARTBEAT_MISSABLE_MAX = "nifi.cluster.protocol.heartbeat.missable.max";
public static final String CLUSTER_PROTOCOL_IS_SECURE = "nifi.cluster.protocol.is.secure";
public static final String CLUSTER_LEADER_ELECTION_IMPLEMENTATION = "nifi.cluster.leader.election.implementation";
// cluster node properties
public static final String CLUSTER_IS_NODE = "nifi.cluster.is.node";
@ -405,6 +406,7 @@ public class NiFiProperties extends ApplicationProperties {
public static final String DEFAULT_CLUSTER_NODE_READ_TIMEOUT = "5 sec";
public static final String DEFAULT_CLUSTER_NODE_CONNECTION_TIMEOUT = "5 sec";
public static final int DEFAULT_CLUSTER_NODE_MAX_CONCURRENT_REQUESTS = 100;
public static final String DEFAULT_CLUSTER_LEADER_ELECTION_IMPLEMENTATION = "CuratorLeaderElectionManager";
// cluster node defaults
public static final int DEFAULT_CLUSTER_NODE_PROTOCOL_THREADS = 10;

View File

@ -33,6 +33,7 @@
<module>nifi-hl7-query-language</module>
<module>nifi-json-utils</module>
<module>nifi-jetty-configuration</module>
<module>nifi-kubernetes-client</module>
<module>nifi-logging-utils</module>
<module>nifi-metrics</module>
<module>nifi-parameter</module>

View File

@ -89,6 +89,10 @@ prop_replace 'nifi.cluster.flow.election.max.wait.time' "${NIFI_ELECTION_MAX
prop_replace 'nifi.cluster.flow.election.max.candidates' "${NIFI_ELECTION_MAX_CANDIDATES:-}"
prop_replace 'nifi.web.proxy.context.path' "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
# Set leader election and state management properties
prop_replace 'nifi.cluster.leader.election.implementation' "${NIFI_LEADER_ELECTION_IMPLEMENTATION:-CuratorLeaderElectionManager}"
prop_replace 'nifi.state.management.provider.cluster' "${NIFI_STATE_MANAGEMENT_CLUSTER_PROVIDER:-zk-provider}"
# Set analytics properties
prop_replace 'nifi.analytics.predict.enabled' "${NIFI_ANALYTICS_PREDICT_ENABLED:-false}"
prop_replace 'nifi.analytics.predict.interval' "${NIFI_ANALYTICS_PREDICT_INTERVAL:-3 mins}"

View File

@ -4193,6 +4193,16 @@ Configure these properties for cluster nodes.
|====
|*Property*|*Description*
|`nifi.cluster.is.node`|Set this to `true` if the instance is a node in a cluster. The default value is `false`.
|`nifi.cluster.leader.election.implementation`|The Cluster Leader Election implementation class name or simple class
name.
The default value is `CuratorLeaderElectionManager` for ZooKeeper Leader Election using `nifi.zookeeper` settings.
The implementation can be set to `KubernetesLeaderElectionManager` for Leader Election using
link:https://kubernetes.io/docs/concepts/architecture/leases/[Kubernetes Leases]. The Kubernetes namespace for Leases
will be read from the
link:https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/[Service Account] namespace secret.
The Kubernetes namespace will be set to `default` if the Service Account secret is not found.
|`nifi.cluster.node.address`|The fully qualified address of the node. It is blank by default.
|`nifi.cluster.node.protocol.port`|The node's protocol port. It is blank by default.
|`nifi.cluster.node.protocol.max.threads`|The maximum number of threads that should be used to communicate with other nodes in the cluster. This property defaults to `50`. When a request is made to one node, it must be forwarded to the coordinator. The coordinator then replicates it to all nodes. There could be up to `n+2` threads for a given request, where `n` = number of nodes in your cluster. As an example, if 4 requests are made, a 5 node cluster will use `4 * 7 = 28` threads.

View File

@ -14,12 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.controller.leader.election;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* Leader Election Manager abstracts cluster election registration and status
*/
public interface LeaderElectionManager {
/**
* Starts managing leader elections for all registered roles
@ -62,10 +65,10 @@ public interface LeaderElectionManager {
* known or if the role was registered without providing a Participant ID, this will return <code>null</code>.
*
* @param roleName the name of the role
* @return the Participant ID of the node that is elected leader, or <code>null</code> if either no leader is known or the leader
* @return the Participant ID of the node that is elected leader, or <code>empty</code> if either no leader is known or the leader
* did not register with a Participant ID.
*/
String getLeader(String roleName);
Optional<String> getLeader(String roleName);
/**
* Removes the role with the given name from this manager. If this
@ -85,11 +88,6 @@ public interface LeaderElectionManager {
*/
boolean isLeader(String roleName);
/**
* @return <code>true</code> if the manager is stopped, false otherwise.
*/
boolean isStopped();
/**
* Stops managing leader elections and relinquishes the role as leader
* for all registered roles. If the LeaderElectionManager is later started
@ -97,14 +95,6 @@ public interface LeaderElectionManager {
*/
void stop();
/**
* Returns <code>true</code> if a leader has been elected for the given role, <code>false</code> otherwise.
*
* @param roleName the name of the role
* @return <code>true</code> if a leader has been elected, <code>false</code> otherwise.
*/
boolean isLeaderElected(String roleName);
/**
* Returns a Map of Role Name to the number of times that the leader has been detected as changing in the given time period. Note that
* the amount of time that these counts is stored and the precision is implementation specific.

View File

@ -14,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.controller.leader.election;
/**
@ -24,12 +23,12 @@ package org.apache.nifi.controller.leader.election;
*/
public interface LeaderElectionStateChangeListener {
/**
* This method is invoked whenever this node is elected leader
* Invoked when the running node is elected as leader
*/
void onLeaderElection();
void onStartLeading();
/**
* This method is invoked whenever this node no longer is the elected leader.
* Invoked when the running node is no longer elected as leader
*/
void onLeaderRelinquish();
void onStopLeading();
}

View File

@ -29,6 +29,9 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MockStateManager implements StateManager {
private final AtomicInteger versionIndex = new AtomicInteger(0);
@ -260,7 +263,7 @@ public class MockStateManager implements StateManager {
*/
public void assertStateSet(final Scope scope) {
final StateMap stateMap = (scope == Scope.CLUSTER) ? clusterStateMap : localStateMap;
Assertions.assertNotSame(-1L, stateMap.getVersion(), "Expected state to be set for Scope " + scope + ", but it was not set");
assertTrue(stateMap.getStateVersion().isPresent(), "Expected state to be set for Scope " + scope + ", but it was not set");
}
/**
@ -278,7 +281,7 @@ public class MockStateManager implements StateManager {
*/
public void assertStateNotSet(final Scope scope) {
final StateMap stateMap = (scope == Scope.CLUSTER) ? clusterStateMap : localStateMap;
Assertions.assertEquals(-1L, stateMap.getVersion(), "Expected state not to be set for Scope " + scope + ", but it was set");
assertFalse(stateMap.getStateVersion().isPresent(), "Expected state not to be set for Scope " + scope + ", but it was set");
}
/**

View File

@ -414,7 +414,7 @@ public class ListS3 extends AbstractS3Processor implements VerifiableProcessor {
private void restoreState(final ProcessSession session) throws IOException {
final StateMap stateMap = session.getState(Scope.CLUSTER);
if (stateMap.getVersion() == -1L || stateMap.get(CURRENT_TIMESTAMP) == null || stateMap.get(CURRENT_KEY_PREFIX+"0") == null) {
if (!stateMap.getStateVersion().isPresent() || stateMap.get(CURRENT_TIMESTAMP) == null || stateMap.get(CURRENT_KEY_PREFIX+"0") == null) {
forcefullyUpdateListing(0L, Collections.emptySet());
} else {
final long timestamp = Long.parseLong(stateMap.get(CURRENT_TIMESTAMP));

View File

@ -398,7 +398,7 @@ public abstract class AbstractListProcessor<T extends ListableEntity> extends Ab
// Check if state already exists for this path. If so, we have already migrated the state.
final StateMap stateMap = context.getStateManager().getState(getStateScope(context));
if (stateMap.getVersion() == -1L) {
if (!stateMap.getStateVersion().isPresent()) {
try {
// Migrate state from the old way of managing state (distributed cache service and local file)
// to the new mechanism (State Manager).

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-cluster-zookeeper</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<!-- metrics-core required for ZooKeeper Server -->
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<!-- snappy-java required for ZooKeeper Server -->
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.nifi.controller.cluster;
package org.apache.nifi.framework.cluster.zookeeper;
import org.apache.curator.utils.ZookeeperFactory;
import org.apache.zookeeper.Watcher;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.controller.cluster;
package org.apache.nifi.framework.cluster.zookeeper;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.util.FormatUtils;

View File

@ -28,7 +28,7 @@ import org.apache.nifi.cluster.protocol.AbstractNodeProtocolSender;
import org.apache.nifi.cluster.protocol.ProtocolContext;
import org.apache.nifi.cluster.protocol.ProtocolException;
import org.apache.nifi.cluster.protocol.message.ProtocolMessage;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.io.socket.SocketConfiguration;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.KeeperException.NoNodeException;

View File

@ -17,10 +17,9 @@
package org.apache.nifi.cluster.coordination.node;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.cluster.exception.NoClusterCoordinatorException;
import org.apache.nifi.cluster.protocol.AbstractNodeProtocolSender;
import org.apache.nifi.cluster.protocol.ProtocolContext;
@ -42,16 +41,17 @@ public class LeaderElectionNodeProtocolSender extends AbstractNodeProtocolSender
}
@Override
protected InetSocketAddress getServiceAddress() throws IOException {
final String address = electionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
protected InetSocketAddress getServiceAddress() {
final Optional<String> leaderAddress = electionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
if (StringUtils.isEmpty(address)) {
throw new NoClusterCoordinatorException("No node has yet been elected Cluster Coordinator. Cannot establish connection to cluster yet.");
if (!leaderAddress.isPresent()) {
throw new NoClusterCoordinatorException("No node has yet been elected Cluster Coordinator. Cannot establish connection to cluster");
}
final String address = leaderAddress.get();
final String[] splits = address.split(":");
if (splits.length != 2) {
final String message = String.format("Attempted to determine Cluster Coordinator address. Zookeeper indicates "
final String message = String.format("Attempted to determine Cluster Coordinator address. Manager indicates "
+ "that address is %s, but this is not in the expected format of <hostname>:<port>", address);
logger.error(message);
throw new ProtocolException(message);
@ -67,14 +67,13 @@ public class LeaderElectionNodeProtocolSender extends AbstractNodeProtocolSender
throw new NumberFormatException("Port must be in the range of 1 - 65535 but got " + port);
}
} catch (final NumberFormatException nfe) {
final String message = String.format("Attempted to determine Cluster Coordinator address. Zookeeper indicates "
final String message = String.format("Attempted to determine Cluster Coordinator address. Manager indicates "
+ "that address is %s, but the port is not a valid port number", address);
logger.error(message);
throw new ProtocolException(message);
}
final InetSocketAddress socketAddress = InetSocketAddress.createUnresolved(hostname, port);
return socketAddress;
return InetSocketAddress.createUnresolved(hostname, port);
}
}

View File

@ -84,6 +84,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@ -146,7 +147,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
this.leaderElectionManager = leaderElectionManager;
this.flowElection = flowElection;
this.nodeProtocolSender = nodeProtocolSender;
this.stateManager = stateManagerProvider.getStateManager("Cluster Coordinator");
this.stateManager = stateManagerProvider.getStateManager(ClusterRoles.CLUSTER_COORDINATOR);
recoverState();
@ -250,7 +251,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
if (localId != null) {
final NodeConnectionStatus shutdownStatus = new NodeConnectionStatus(localId, DisconnectionCode.NODE_SHUTDOWN);
updateNodeStatus(shutdownStatus, false);
logger.info("Successfully notified other nodes that I am shutting down");
logger.info("Node ID [{}] Disconnection Code [{}] send completed", localId, DisconnectionCode.NODE_SHUTDOWN);
}
}
@ -297,7 +298,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
return localNodeId;
}
private String getElectedActiveCoordinatorAddress() {
private Optional<String> getElectedActiveCoordinatorAddress() {
return leaderElectionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
}
@ -726,14 +727,14 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
@Override
public NodeIdentifier getPrimaryNode() {
final String primaryNodeAddress = leaderElectionManager.getLeader(ClusterRoles.PRIMARY_NODE);
if (primaryNodeAddress == null) {
final Optional<String> primaryNodeLeader = leaderElectionManager.getLeader(ClusterRoles.PRIMARY_NODE);
if (!primaryNodeLeader.isPresent()) {
return null;
}
return nodeStatuses.values().stream()
.map(NodeConnectionStatus::getNodeIdentifier)
.filter(nodeId -> primaryNodeAddress.equals(nodeId.getSocketAddress() + ":" + nodeId.getSocketPort()))
.filter(nodeId -> primaryNodeLeader.get().equals(nodeId.getSocketAddress() + ":" + nodeId.getSocketPort()))
.findFirst()
.orElse(null);
}
@ -744,25 +745,25 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
}
private NodeIdentifier getElectedActiveCoordinatorNode(final boolean warnOnError) {
String electedNodeAddress;
final Optional<String> electedActiveCoordinatorAddress;
try {
electedNodeAddress = getElectedActiveCoordinatorAddress();
electedActiveCoordinatorAddress = getElectedActiveCoordinatorAddress();
} catch (final NoClusterCoordinatorException ncce) {
logger.debug("There is currently no elected active Cluster Coordinator");
return null;
}
if (electedNodeAddress == null || electedNodeAddress.trim().isEmpty()) {
if (!electedActiveCoordinatorAddress.isPresent()) {
logger.debug("There is currently no elected active Cluster Coordinator");
return null;
}
electedNodeAddress = electedNodeAddress.trim();
final String electedNodeAddress = electedActiveCoordinatorAddress.get().trim();
final int colonLoc = electedNodeAddress.indexOf(':');
if (colonLoc < 1) {
if (warnOnError) {
logger.warn("Failed to determine which node is elected active Cluster Coordinator: ZooKeeper reports the address as {}, but this is not a valid address", electedNodeAddress);
logger.warn("Failed to determine which node is elected active Cluster Coordinator: Manager reports the address as {}, but this is not a valid address", electedNodeAddress);
}
return null;
@ -775,7 +776,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
electedNodePort = Integer.parseInt(portString);
} catch (final NumberFormatException nfe) {
if (warnOnError) {
logger.warn("Failed to determine which node is elected active Cluster Coordinator: ZooKeeper reports the address as {}, but this is not a valid address", electedNodeAddress);
logger.warn("Failed to determine which node is elected active Cluster Coordinator: Manager reports the address as {}, but this is not a valid address", electedNodeAddress);
}
return null;
@ -788,7 +789,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
.orElse(null);
if (electedNodeId == null && warnOnError) {
logger.debug("Failed to determine which node is elected active Cluster Coordinator: ZooKeeper reports the address as {},"
logger.debug("Failed to determine which node is elected active Cluster Coordinator: Manager reports the address as {},"
+ "but there is no node with this address. Will attempt to communicate with node to determine its information", electedNodeAddress);
try {
@ -807,7 +808,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
return existingStatus.getNodeIdentifier();
}
} catch (final Exception e) {
logger.warn("Failed to determine which node is elected active Cluster Coordinator: ZooKeeper reports the address as {}, but there is no node with this address. "
logger.warn("Failed to determine which node is elected active Cluster Coordinator: Manager reports the address as {}, but there is no node with this address. "
+ "Attempted to determine the node's information but failed to retrieve its information due to {}", electedNodeAddress, e.toString());
if (logger.isDebugEnabled()) {
@ -1446,7 +1447,7 @@ public class NodeClusterCoordinator implements ClusterCoordinator, ProtocolHandl
}
@Override
public Map<NodeIdentifier, NodeWorkload> getClusterWorkload() throws IOException {
public Map<NodeIdentifier, NodeWorkload> getClusterWorkload() {
final ClusterWorkloadRequestMessage request = new ClusterWorkloadRequestMessage();
final ClusterWorkloadResponseMessage response = nodeProtocolSender.clusterWorkload(request);
return response.getNodeWorkloads();

View File

@ -37,8 +37,8 @@
<!-- Leader Election Manager -->
<bean id="leaderElectionManager" class="org.apache.nifi.spring.LeaderElectionManagerFactoryBean">
<property name="numThreads" value="4" />
<property name="properties" ref="nifiProperties" />
<property name="extensionManager" ref="extensionManager" />
</bean>
<bean id="flowElection" class="org.apache.nifi.cluster.coordination.flow.PopularVoteFlowElectionFactoryBean">

View File

@ -1,99 +0,0 @@
/*
* 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.cluster.coordination.node
import org.apache.nifi.cluster.coordination.flow.FlowElection
import org.apache.nifi.cluster.firewall.ClusterNodeFirewall
import org.apache.nifi.cluster.protocol.NodeIdentifier
import org.apache.nifi.cluster.protocol.NodeProtocolSender
import org.apache.nifi.cluster.protocol.impl.ClusterCoordinationProtocolSenderListener
import org.apache.nifi.cluster.protocol.message.OffloadMessage
import org.apache.nifi.components.state.Scope
import org.apache.nifi.components.state.StateManager
import org.apache.nifi.components.state.StateManagerProvider
import org.apache.nifi.controller.leader.election.LeaderElectionManager
import org.apache.nifi.events.EventReporter
import org.apache.nifi.reporting.Severity
import org.apache.nifi.state.MockStateMap
import org.apache.nifi.util.NiFiProperties
import org.apache.nifi.web.revision.RevisionManager
import spock.lang.Specification
import spock.util.concurrent.BlockingVariable
import java.util.concurrent.TimeUnit
class NodeClusterCoordinatorSpec extends Specification {
def "requestNodeOffload"() {
given: 'mocked collaborators'
def clusterCoordinationProtocolSenderListener = Mock(ClusterCoordinationProtocolSenderListener)
def eventReporter = Mock EventReporter
def stateManager = Mock StateManager
def stateMap = new MockStateMap([:], 1)
stateManager.getState(_ as Scope) >> stateMap
def stateManagerProvider = Mock StateManagerProvider
stateManagerProvider.getStateManager(_ as String) >> stateManager
and: 'a NodeClusterCoordinator that manages node status in a synchronized list'
List<NodeConnectionStatus> nodeStatuses = [].asSynchronized()
def clusterCoordinator = new NodeClusterCoordinator(clusterCoordinationProtocolSenderListener, eventReporter, Mock(LeaderElectionManager),
Mock(FlowElection), Mock(ClusterNodeFirewall),
Mock(RevisionManager), NiFiProperties.createBasicNiFiProperties('src/test/resources/conf/nifi.properties', [:]),
Mock(NodeProtocolSender), stateManagerProvider) {
@Override
void notifyOthersOfNodeStatusChange(NodeConnectionStatus updatedStatus, boolean notifyAllNodes, boolean waitForCoordinator) {
nodeStatuses.add(updatedStatus)
}
}
and: 'two nodes'
def nodeIdentifier1 = createNodeIdentifier 1
def nodeIdentifier2 = createNodeIdentifier 2
and: 'node 1 is connected, node 2 is disconnected'
clusterCoordinator.updateNodeStatus new NodeConnectionStatus(nodeIdentifier1, NodeConnectionState.CONNECTED)
clusterCoordinator.updateNodeStatus new NodeConnectionStatus(nodeIdentifier2, NodeConnectionState.DISCONNECTED)
while (nodeStatuses.size() < 2) {
Thread.sleep(10)
}
nodeStatuses.clear()
def waitForReportEvent = new BlockingVariable(5, TimeUnit.SECONDS)
when: 'a node is requested to offload'
clusterCoordinator.requestNodeOffload nodeIdentifier2, OffloadCode.OFFLOADED, 'unit test for offloading node'
waitForReportEvent.get()
then: 'no exceptions are thrown'
noExceptionThrown()
and: 'expected methods on collaborators are invoked'
1 * clusterCoordinationProtocolSenderListener.offload({ OffloadMessage msg -> msg.nodeId == nodeIdentifier2 } as OffloadMessage)
1 * eventReporter.reportEvent(Severity.INFO, 'Clustering', { msg -> msg.contains "$nodeIdentifier2.apiAddress:$nodeIdentifier2.apiPort" } as String) >> {
waitForReportEvent.set(it)
}
and: 'the status of the offloaded node is known by the cluster coordinator to be offloading'
nodeStatuses[0].nodeIdentifier == nodeIdentifier2
nodeStatuses[0].state == NodeConnectionState.OFFLOADING
}
private static NodeIdentifier createNodeIdentifier(final int index) {
new NodeIdentifier("node-id-$index", "localhost", 8000 + index, "localhost", 9000 + index,
"localhost", 10000 + index, 11000 + index, false)
}
}

View File

@ -1,50 +0,0 @@
/*
* 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.cluster.integration
import org.apache.nifi.cluster.coordination.node.DisconnectionCode
import org.apache.nifi.cluster.coordination.node.OffloadCode
import spock.lang.Specification
import java.util.concurrent.TimeUnit
class OffloadNodeITSpec extends Specification {
def "requestNodeOffload"() {
given: 'a cluster with 3 nodes'
System.setProperty 'nifi.properties.file.path', 'src/test/resources/conf/nifi.properties'
def cluster = new Cluster()
cluster.start()
cluster.createNode()
def nodeToOffload = cluster.createNode()
cluster.createNode()
cluster.waitUntilAllNodesConnected 20, TimeUnit.SECONDS
when: 'the node to offload is disconnected successfully'
cluster.currentClusterCoordinator.clusterCoordinator.requestNodeDisconnect nodeToOffload.identifier, DisconnectionCode.USER_DISCONNECTED,
'integration test user disconnect'
cluster.currentClusterCoordinator.assertNodeDisconnects nodeToOffload.identifier, 10, TimeUnit.SECONDS
and: 'the node to offload is requested to offload'
nodeToOffload.getClusterCoordinator().requestNodeOffload nodeToOffload.identifier, OffloadCode.OFFLOADED, 'integration test offload'
then: 'the node has been successfully offloaded'
cluster.currentClusterCoordinator.assertNodeIsOffloaded nodeToOffload.identifier, 10, TimeUnit.SECONDS
cleanup:
cluster.stop()
}
}

View File

@ -1,172 +0,0 @@
/*
* 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.cluster.integration;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.curator.test.TestingServer;
import org.apache.nifi.cluster.coordination.flow.FlowElection;
import org.apache.nifi.cluster.coordination.flow.PopularVoteFlowElection;
import org.apache.nifi.cluster.coordination.node.ClusterRoles;
import org.apache.nifi.controller.status.history.StatusHistoryRepository;
import org.apache.nifi.controller.status.history.VolatileComponentStatusRepository;
import org.apache.nifi.nar.ExtensionDiscoveringManager;
import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class Cluster {
private static final Logger logger = LoggerFactory.getLogger(Cluster.class);
private final Set<Node> nodes = new HashSet<>();
private final TestingServer zookeeperServer;
private final long flowElectionTimeoutMillis;
private final Integer flowElectionMaxNodes;
public Cluster() throws IOException {
this(3, TimeUnit.SECONDS, 3);
}
public Cluster(final long flowElectionTimeout, final TimeUnit flowElectionTimeUnit, final Integer flowElectionMaxNodes) throws IOException {
try {
zookeeperServer = new TestingServer();
} catch (final Exception e) {
throw new RuntimeException(e);
}
this.flowElectionTimeoutMillis = flowElectionTimeUnit.toMillis(flowElectionTimeout);
this.flowElectionMaxNodes = flowElectionMaxNodes;
}
public void start() {
try {
zookeeperServer.start();
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new RuntimeException(e);
}
while (getZooKeeperConnectString() == null) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
}
}
logger.info("Start ZooKeeper Server on Port {}, with temporary directory {}", zookeeperServer.getPort(), zookeeperServer.getTempDirectory());
}
public void stop() {
for (final Node node : nodes) {
try {
if (node.isRunning()) {
node.stop();
}
} catch (Exception e) {
logger.error("Failed to shut down " + node, e);
}
}
try {
zookeeperServer.stop();
zookeeperServer.close();
} catch (final Exception e) {
}
}
public String getZooKeeperConnectString() {
return zookeeperServer.getConnectString();
}
public Set<Node> getNodes() {
return Collections.unmodifiableSet(nodes);
}
public CuratorFramework createCuratorClient() {
final RetryPolicy retryPolicy = new RetryNTimes(20, 500);
final CuratorFramework curatorClient = CuratorFrameworkFactory.builder()
.connectString(getZooKeeperConnectString())
.sessionTimeoutMs(3000)
.connectionTimeoutMs(3000)
.retryPolicy(retryPolicy)
.defaultData(new byte[0])
.build();
curatorClient.start();
return curatorClient;
}
public Node createNode() {
final Map<String, String> addProps = new HashMap<>();
addProps.put(NiFiProperties.ZOOKEEPER_CONNECT_STRING, getZooKeeperConnectString());
addProps.put(NiFiProperties.CLUSTER_IS_NODE, "true");
final NiFiProperties nifiProperties = NiFiProperties.createBasicNiFiProperties("src/test/resources/conf/nifi.properties", addProps);
final ExtensionDiscoveringManager extensionManager = new StandardExtensionDiscoveringManager();
final FlowElection flowElection = new PopularVoteFlowElection(flowElectionTimeoutMillis, TimeUnit.MILLISECONDS, flowElectionMaxNodes);
final StatusHistoryRepository statusHistoryRepository = new VolatileComponentStatusRepository(nifiProperties);
final Node node = new Node(nifiProperties, extensionManager, flowElection, statusHistoryRepository);
node.start();
nodes.add(node);
return node;
}
public Node getCurrentClusterCoordinator() {
return getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null);
}
public Node waitForClusterCoordinator(final long time, final TimeUnit timeUnit) {
return ClusterUtils.waitUntilNonNull(time, timeUnit,
() -> getNodes().stream().filter(node -> node.hasRole(ClusterRoles.CLUSTER_COORDINATOR)).findFirst().orElse(null));
}
public Node waitForPrimaryNode(final long time, final TimeUnit timeUnit) {
return ClusterUtils.waitUntilNonNull(time, timeUnit,
() -> getNodes().stream().filter(node -> node.hasRole(ClusterRoles.PRIMARY_NODE)).findFirst().orElse(null));
}
/**
* Waits for each node in the cluster to connect. The time given is the maximum amount of time to wait for each node to connect, not for
* the entire cluster to connect.
*
* @param time the max amount of time to wait for a node to connect
* @param timeUnit the unit of time that the given <code>time</code> value represents
*/
public void waitUntilAllNodesConnected(final long time, final TimeUnit timeUnit) {
for (final Node node : nodes) {
node.waitUntilConnected(time, timeUnit);
}
}
}

View File

@ -1,248 +0,0 @@
/*
* 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.cluster.integration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.nifi.cluster.coordination.node.ClusterRoles;
import org.apache.nifi.cluster.coordination.node.DisconnectionCode;
import org.apache.nifi.cluster.coordination.node.NodeConnectionState;
import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus;
import org.apache.nifi.cluster.protocol.NodeIdentifier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
public class ClusterConnectionIT {
private Cluster cluster;
@BeforeAll
public static void setup() {
System.setProperty("nifi.properties.file.path", "src/test/resources/conf/nifi.properties");
}
@BeforeEach
public void createCluster() throws IOException {
cluster = new Cluster();
cluster.start();
}
@AfterEach
public void destroyCluster() {
if (cluster != null) {
cluster.stop();
}
}
@Test
@Timeout(value = 20)
public void testSingleNode() {
final Node firstNode = cluster.createNode();
firstNode.waitUntilConnected(10, TimeUnit.SECONDS);
firstNode.waitUntilElectedForRole(ClusterRoles.CLUSTER_COORDINATOR, 10, TimeUnit.SECONDS);
firstNode.waitUntilElectedForRole(ClusterRoles.PRIMARY_NODE, 10, TimeUnit.SECONDS);
}
@Test
@Timeout(value = 60)
public void testThreeNodeCluster() {
cluster.createNode();
cluster.createNode();
cluster.createNode();
cluster.waitUntilAllNodesConnected(10, TimeUnit.SECONDS);
final Node clusterCoordinator = cluster.waitForClusterCoordinator(10, TimeUnit.SECONDS);
final Node primaryNode = cluster.waitForPrimaryNode(10, TimeUnit.SECONDS);
System.out.println("\n\n");
System.out.println("Cluster Coordinator = " + clusterCoordinator);
System.out.println("Primary Node = " + primaryNode);
System.out.println("\n\n");
}
@Test
@Timeout(value = 60)
public void testNewCoordinatorElected() throws IOException {
final Node firstNode = cluster.createNode();
final Node secondNode = cluster.createNode();
cluster.waitUntilAllNodesConnected(10, TimeUnit.SECONDS);
final Node clusterCoordinator = cluster.waitForClusterCoordinator(10, TimeUnit.SECONDS);
clusterCoordinator.stop();
final Node otherNode = firstNode == clusterCoordinator ? secondNode : firstNode;
otherNode.waitUntilElectedForRole(ClusterRoles.CLUSTER_COORDINATOR, 10, TimeUnit.SECONDS);
}
@Test
@Timeout(value = 60)
public void testReconnectGetsCorrectClusterTopology() throws IOException {
final Node firstNode = cluster.createNode();
final Node secondNode = cluster.createNode();
final Node thirdNode = cluster.createNode();
cluster.waitUntilAllNodesConnected(10, TimeUnit.SECONDS);
// shutdown node
secondNode.stop();
System.out.println("\n\nNode 2 Shut Down\n\n");
// wait for node 1 and 3 to recognize that node 2 is gone
Stream.of(firstNode, thirdNode).forEach(node -> {
node.assertNodeDisconnects(secondNode.getIdentifier(), 10, TimeUnit.SECONDS);
});
// restart node
secondNode.start();
System.out.println("\n\nNode 2 Restarted\n\n");
secondNode.waitUntilConnected(20, TimeUnit.SECONDS);
System.out.println("\n\nNode 2 Reconnected\n\n");
// wait for all 3 nodes to agree that node 2 is connected
Stream.of(firstNode, secondNode, thirdNode).forEach(node -> {
ClusterUtils.waitUntilConditionMet(5, TimeUnit.SECONDS,
() -> firstNode.getClusterCoordinator().getConnectionStatus(secondNode.getIdentifier()).getState() == NodeConnectionState.CONNECTED);
});
// Ensure that all 3 nodes see a cluster of 3 connected nodes.
Stream.of(firstNode, secondNode, thirdNode).forEach(node -> {
node.assertNodeIsConnected(firstNode.getIdentifier());
node.assertNodeIsConnected(secondNode.getIdentifier());
node.assertNodeIsConnected(thirdNode.getIdentifier());
});
// Ensure that we get both a cluster coordinator and a primary node elected
cluster.waitForClusterCoordinator(10, TimeUnit.SECONDS);
cluster.waitForPrimaryNode(10, TimeUnit.SECONDS);
}
@Test
@Timeout(value = 60)
public void testRestartAllNodes() throws IOException {
final Node firstNode = cluster.createNode();
final Node secondNode = cluster.createNode();
final Node thirdNode = cluster.createNode();
firstNode.waitUntilConnected(10, TimeUnit.SECONDS);
System.out.println("**** Node 1 Connected ****");
secondNode.waitUntilConnected(10, TimeUnit.SECONDS);
System.out.println("**** Node 2 Connected ****");
thirdNode.waitUntilConnected(10, TimeUnit.SECONDS);
System.out.println("**** Node 3 Connected ****");
// shutdown node
firstNode.stop();
secondNode.stop();
thirdNode.stop();
System.out.println("\n\nRestarting all nodes\n\n");
thirdNode.start();
firstNode.start();
secondNode.start();
firstNode.waitUntilConnected(20, TimeUnit.SECONDS);
System.out.println("\n\n\n**** Node 1 Re-Connected ****\n\n\n");
secondNode.waitUntilConnected(10, TimeUnit.SECONDS);
System.out.println("**** Node 2 Re-Connected ****");
thirdNode.waitUntilConnected(10, TimeUnit.SECONDS);
System.out.println("**** Node 3 Re-Connected ****");
// wait for all 3 nodes to agree that node 2 is connected
Stream.of(firstNode, secondNode, thirdNode).forEach(node -> {
ClusterUtils.waitUntilConditionMet(5, TimeUnit.SECONDS,
() -> firstNode.getClusterCoordinator().getConnectionStatus(secondNode.getIdentifier()).getState() == NodeConnectionState.CONNECTED);
});
// Ensure that all 3 nodes see a cluster of 3 connected nodes.
Stream.of(firstNode, secondNode, thirdNode).forEach(node -> {
node.assertNodeConnects(firstNode.getIdentifier(), 10, TimeUnit.SECONDS);
node.assertNodeConnects(secondNode.getIdentifier(), 10, TimeUnit.SECONDS);
node.assertNodeConnects(thirdNode.getIdentifier(), 10, TimeUnit.SECONDS);
});
// Ensure that we get both a cluster coordinator and a primary node elected
cluster.waitForClusterCoordinator(10, TimeUnit.SECONDS);
cluster.waitForPrimaryNode(10, TimeUnit.SECONDS);
}
@Test
@Timeout(value = 30)
public void testHeartbeatsMonitored() {
final Node firstNode = cluster.createNode();
final Node secondNode = cluster.createNode();
cluster.waitUntilAllNodesConnected(10, TimeUnit.SECONDS);
final Node nodeToSuspend = firstNode;
final Node otherNode = secondNode;
nodeToSuspend.suspendHeartbeating();
// Heartbeat interval in nifi.properties is set to 1 sec. This means that the node should be kicked out
// due to lack of heartbeat after 8 times this amount of time, or 8 seconds.
otherNode.assertNodeDisconnects(nodeToSuspend.getIdentifier(), 12, TimeUnit.SECONDS);
nodeToSuspend.resumeHeartbeating();
otherNode.assertNodeConnects(nodeToSuspend.getIdentifier(), 10, TimeUnit.SECONDS);
}
@Test
@Timeout(value = 60)
public void testNodeInheritsClusterTopologyOnHeartbeat() throws InterruptedException {
final Node node1 = cluster.createNode();
final Node node2 = cluster.createNode();
final Node node3 = cluster.createNode();
cluster.waitUntilAllNodesConnected(10, TimeUnit.SECONDS);
final Node coordinator = cluster.waitForClusterCoordinator(10, TimeUnit.SECONDS);
final NodeIdentifier node4NotReallyInCluster = new NodeIdentifier(UUID.randomUUID().toString(), "localhost", 9283, "localhost", 9284, "localhost", 9286, "localhost", 9285, null, false, null);
final Map<NodeIdentifier, NodeConnectionStatus> replacementStatuses = new HashMap<>();
replacementStatuses.put(node1.getIdentifier(), new NodeConnectionStatus(node1.getIdentifier(), DisconnectionCode.USER_DISCONNECTED));
replacementStatuses.put(node4NotReallyInCluster, new NodeConnectionStatus(node4NotReallyInCluster, NodeConnectionState.CONNECTING));
// reset coordinator status so that other nodes with get its now-fake view of the cluster
coordinator.getClusterCoordinator().resetNodeStatuses(replacementStatuses);
final List<NodeConnectionStatus> expectedStatuses = coordinator.getClusterCoordinator().getConnectionStatuses();
// give nodes a bit to heartbeat in. We need to wait long enough that each node heartbeats.
// But we need to not wait more than 8 seconds because that's when nodes start getting kicked out.
Thread.sleep(6000L);
for (final Node node : new Node[] {node1, node2, node3}) {
assertEquals(expectedStatuses, node.getClusterCoordinator().getConnectionStatuses());
}
}
}

View File

@ -1,60 +0,0 @@
/*
* 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.cluster.integration;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
public class ClusterUtils {
public static void waitUntilConditionMet(final long time, final TimeUnit timeUnit, final BooleanSupplier test) {
waitUntilConditionMet(time, timeUnit, test, null);
}
public static void waitUntilConditionMet(final long time, final TimeUnit timeUnit, final BooleanSupplier test, final Supplier<String> errorMessageSupplier) {
final long nanosToWait = timeUnit.toNanos(time);
final long start = System.nanoTime();
final long maxTime = start + nanosToWait;
while (!test.getAsBoolean()) {
if (System.nanoTime() > maxTime) {
if (errorMessageSupplier == null) {
throw new AssertionError("Condition never occurred after waiting " + time + " " + timeUnit);
} else {
throw new AssertionError("Condition never occurred after waiting " + time + " " + timeUnit + " : " + errorMessageSupplier.get());
}
}
}
}
public static <T> T waitUntilNonNull(final long time, final TimeUnit timeUnit, final Supplier<T> test) {
final long nanosToWait = timeUnit.toNanos(time);
final long start = System.nanoTime();
final long maxTime = start + nanosToWait;
T returnVal;
while ((returnVal = test.get()) == null) {
if (System.nanoTime() > maxTime) {
throw new AssertionError("Condition never occurred after waiting " + time + " " + timeUnit);
}
}
return returnVal;
}
}

View File

@ -1,401 +0,0 @@
/*
* 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.cluster.integration;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.nifi.authorization.Authorizer;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.cluster.ReportedEvent;
import org.apache.nifi.cluster.coordination.flow.FlowElection;
import org.apache.nifi.cluster.coordination.heartbeat.ClusterProtocolHeartbeatMonitor;
import org.apache.nifi.cluster.coordination.heartbeat.HeartbeatMonitor;
import org.apache.nifi.cluster.coordination.node.LeaderElectionNodeProtocolSender;
import org.apache.nifi.cluster.coordination.node.NodeClusterCoordinator;
import org.apache.nifi.cluster.coordination.node.NodeConnectionState;
import org.apache.nifi.cluster.coordination.node.NodeConnectionStatus;
import org.apache.nifi.cluster.protocol.ClusterCoordinationProtocolSender;
import org.apache.nifi.cluster.protocol.NodeIdentifier;
import org.apache.nifi.cluster.protocol.NodeProtocolSender;
import org.apache.nifi.cluster.protocol.ProtocolContext;
import org.apache.nifi.cluster.protocol.ProtocolListener;
import org.apache.nifi.cluster.protocol.impl.ClusterCoordinationProtocolSenderListener;
import org.apache.nifi.cluster.protocol.impl.NodeProtocolSenderListener;
import org.apache.nifi.cluster.protocol.impl.SocketProtocolListener;
import org.apache.nifi.cluster.protocol.impl.StandardClusterCoordinationProtocolSender;
import org.apache.nifi.cluster.protocol.jaxb.JaxbProtocolContext;
import org.apache.nifi.cluster.protocol.jaxb.message.JaxbProtocolUtils;
import org.apache.nifi.cluster.protocol.message.ProtocolMessage;
import org.apache.nifi.components.state.Scope;
import org.apache.nifi.controller.FlowController;
import org.apache.nifi.controller.StandardFlowService;
import org.apache.nifi.controller.leader.election.CuratorLeaderElectionManager;
import org.apache.nifi.controller.leader.election.LeaderElectionManager;
import org.apache.nifi.controller.repository.FlowFileEventRepository;
import org.apache.nifi.controller.status.history.StatusHistoryRepository;
import org.apache.nifi.encrypt.PropertyEncryptorFactory;
import org.apache.nifi.engine.FlowEngine;
import org.apache.nifi.events.EventReporter;
import org.apache.nifi.io.socket.ServerSocketConfiguration;
import org.apache.nifi.io.socket.SocketConfiguration;
import org.apache.nifi.nar.ExtensionDiscoveringManager;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.SystemBundle;
import org.apache.nifi.registry.VariableRegistry;
import org.apache.nifi.reporting.BulletinRepository;
import org.apache.nifi.reporting.Severity;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.revision.RevisionManager;
import org.apache.nifi.web.revision.RevisionSnapshot;
import org.mockito.Mockito;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class Node {
private final NodeIdentifier nodeId;
private final NiFiProperties nodeProperties;
private final ExtensionManager extensionManager;
private final List<ReportedEvent> reportedEvents = Collections.synchronizedList(new ArrayList<ReportedEvent>());
private final RevisionManager revisionManager;
private final FlowElection flowElection;
private final StatusHistoryRepository statusHistoryRepository;
private NodeClusterCoordinator clusterCoordinator;
private NodeProtocolSender protocolSender;
private FlowController flowController;
private StandardFlowService flowService;
private LeaderElectionManager electionManager;
private ProtocolListener protocolListener;
private volatile boolean running = false;
private ScheduledExecutorService executor = new FlowEngine(8, "Node tasks", true);
public Node(final NiFiProperties properties, final ExtensionDiscoveringManager extensionManager, final FlowElection flowElection,
final StatusHistoryRepository statusHistoryRepository) {
this(createNodeId(), properties, extensionManager, flowElection, statusHistoryRepository);
}
public Node(final NodeIdentifier nodeId, final NiFiProperties properties, final ExtensionDiscoveringManager extensionManager,
final FlowElection flowElection, final StatusHistoryRepository statusHistoryRepository) {
this.nodeId = nodeId;
this.nodeProperties = new NiFiProperties() {
@Override
public String getProperty(String key) {
if(key.equals(NiFiProperties.CLUSTER_NODE_PROTOCOL_PORT)){
return String.valueOf(nodeId.getSocketPort());
}else if(key.equals(NiFiProperties.WEB_HTTP_PORT)){
return String.valueOf(nodeId.getApiPort());
}else if(key.equals(NiFiProperties.LOAD_BALANCE_PORT)){
return String.valueOf(nodeId.getLoadBalancePort());
}else {
return properties.getProperty(key);
}
}
@Override
public Set<String> getPropertyKeys() {
final Set<String> keys = new HashSet<>(properties.getPropertyKeys());
keys.add(NiFiProperties.CLUSTER_NODE_PROTOCOL_PORT);
keys.add(NiFiProperties.WEB_HTTP_PORT);
return keys;
}
};
final Bundle systemBundle = SystemBundle.create(properties);
extensionManager.discoverExtensions(systemBundle, Collections.emptySet());
this.extensionManager = extensionManager;
revisionManager = Mockito.mock(RevisionManager.class);
RevisionSnapshot revisionSnapshot = new RevisionSnapshot(Collections.emptyList(), 0L);
Mockito.when(revisionManager.getAllRevisions()).thenReturn(revisionSnapshot);
electionManager = new CuratorLeaderElectionManager(4, nodeProperties);
this.flowElection = flowElection;
this.statusHistoryRepository = statusHistoryRepository;
}
private static NodeIdentifier createNodeId() {
return new NodeIdentifier(UUID.randomUUID().toString(), "localhost", createPort(), "localhost", createPort(), "localhost", createPort(), "localhost", null, null, false, null);
}
public synchronized void start() {
running = true;
protocolSender = createNodeProtocolSender();
clusterCoordinator = createClusterCoordinator();
clusterCoordinator.setLocalNodeIdentifier(nodeId);
// clusterCoordinator.setConnected(true);
final HeartbeatMonitor heartbeatMonitor = createHeartbeatMonitor();
flowController = FlowController.createClusteredInstance(Mockito.mock(FlowFileEventRepository.class), nodeProperties,
null, null, PropertyEncryptorFactory.getPropertyEncryptor(nodeProperties), protocolSender, Mockito.mock(BulletinRepository.class), clusterCoordinator,
heartbeatMonitor, electionManager, VariableRegistry.EMPTY_REGISTRY, extensionManager, revisionManager, statusHistoryRepository);
try {
flowController.initializeFlow();
} catch (IOException e) {
throw new RuntimeException(e);
}
final NodeProtocolSenderListener senderListener = new NodeProtocolSenderListener(protocolSender, protocolListener);
try {
flowController.getStateManagerProvider().getStateManager("Cluster Node Configuration").setState(Collections.singletonMap("Node UUID", nodeId.getId()), Scope.LOCAL);
flowService = StandardFlowService.createClusteredInstance(flowController, nodeProperties, senderListener, clusterCoordinator,
revisionManager, Mockito.mock(Authorizer.class));
flowService.start();
flowService.load(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void stop() throws IOException {
running = false;
flowController.shutdown(true);
flowService.stop(true);
clusterCoordinator.shutdown();
executor.shutdownNow();
// protocol listener is closed by flow controller
}
public void suspendHeartbeating() {
flowController.suspendHeartbeats();
}
public void resumeHeartbeating() {
flowController.resumeHeartbeats();
}
public NodeIdentifier getIdentifier() {
return nodeId;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(nodeId).build();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof Node)) {
return false;
}
return getIdentifier().equals(((Node) obj).getIdentifier());
}
@Override
public String toString() {
return "Node[id=" + getIdentifier() + ", started=" + isRunning() + "]";
}
public boolean isRunning() {
return running;
}
private static int createPort() {
// get an unused port
while (true) {
try (ServerSocket ss = new ServerSocket(0)) {
return ss.getLocalPort();
} catch (final IOException ioe) {
}
}
}
public NodeConnectionStatus getConnectionStatus() {
return clusterCoordinator.getConnectionStatus(nodeId);
}
@SuppressWarnings("unchecked")
private NodeProtocolSender createNodeProtocolSender() {
final SocketConfiguration socketConfig = new SocketConfiguration();
socketConfig.setSocketTimeout(3000);
socketConfig.setReuseAddress(true);
final ProtocolContext<ProtocolMessage> protocolContext = new JaxbProtocolContext<>(JaxbProtocolUtils.JAXB_CONTEXT);
final NodeProtocolSender protocolSender = new LeaderElectionNodeProtocolSender(socketConfig, protocolContext, electionManager);
return protocolSender;
}
@SuppressWarnings("unchecked")
private ClusterCoordinationProtocolSender createCoordinatorProtocolSender() {
final SocketConfiguration socketConfig = new SocketConfiguration();
socketConfig.setSocketTimeout(3000);
socketConfig.setReuseAddress(true);
final ProtocolContext<ProtocolMessage> protocolContext = new JaxbProtocolContext<>(JaxbProtocolUtils.JAXB_CONTEXT);
return new StandardClusterCoordinationProtocolSender(socketConfig, protocolContext, 1);
}
private HeartbeatMonitor createHeartbeatMonitor() {
return new ClusterProtocolHeartbeatMonitor(clusterCoordinator, protocolListener, nodeProperties);
}
@SuppressWarnings("unchecked")
private NodeClusterCoordinator createClusterCoordinator() {
final EventReporter eventReporter = new EventReporter() {
@Override
public void reportEvent(Severity severity, String category, String message) {
reportedEvents.add(new ReportedEvent(nodeId, severity, message));
}
};
final ServerSocketConfiguration serverSocketConfiguration = new ServerSocketConfiguration();
serverSocketConfiguration.setSocketTimeout(5000);
final ProtocolContext<ProtocolMessage> protocolContext = new JaxbProtocolContext<>(JaxbProtocolUtils.JAXB_CONTEXT);
protocolListener = new SocketProtocolListener(3, Integer.parseInt(nodeProperties.getProperty(NiFiProperties.CLUSTER_NODE_PROTOCOL_PORT)), serverSocketConfiguration, protocolContext);
try {
protocolListener.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
final ClusterCoordinationProtocolSenderListener protocolSenderListener = new ClusterCoordinationProtocolSenderListener(createCoordinatorProtocolSender(), protocolListener);
try {
return new NodeClusterCoordinator(protocolSenderListener, eventReporter, electionManager, flowElection, null,
revisionManager, nodeProperties, extensionManager, protocolSender);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public NodeClusterCoordinator getClusterCoordinator() {
return clusterCoordinator;
}
//
// Methods for checking conditions
//
public boolean isConnected() {
final NodeConnectionStatus status = getConnectionStatus();
if (status == null) {
return false;
}
return status.getState() == NodeConnectionState.CONNECTED;
}
//
// Methods to wait for conditions
//
public void waitUntilConnected(final long time, final TimeUnit timeUnit) {
ClusterUtils.waitUntilConditionMet(time, timeUnit, () -> isConnected());
}
private String getClusterAddress() {
final InetSocketAddress address = nodeProperties.getClusterNodeProtocolAddress();
return address.getHostName() + ":" + address.getPort();
}
public boolean hasRole(final String roleName) {
final String leaderAddress = electionManager.getLeader(roleName);
if (leaderAddress == null) {
return false;
}
return leaderAddress.equals(getClusterAddress());
}
public void waitUntilElectedForRole(final String roleName, final long time, final TimeUnit timeUnit) {
ClusterUtils.waitUntilConditionMet(time, timeUnit, () -> hasRole(roleName));
}
// Assertions
/**
* Assert that the node with the given ID connects (According to this node!) within the given amount of time
*
* @param nodeId id of the node
* @param time how long to wait
* @param timeUnit unit of time provided by the 'time' argument
*/
public void assertNodeConnects(final NodeIdentifier nodeId, final long time, final TimeUnit timeUnit) {
ClusterUtils.waitUntilConditionMet(time, timeUnit,
() -> getClusterCoordinator().getConnectionStatus(nodeId).getState() == NodeConnectionState.CONNECTED,
() -> "Connection Status is " + getClusterCoordinator().getConnectionStatus(nodeId).toString());
}
/**
* Assert that the node with the given ID disconnects (According to this node!) within the given amount of time
*
* @param nodeId id of the node
* @param time how long to wait
* @param timeUnit unit of time provided by the 'time' argument
*/
public void assertNodeDisconnects(final NodeIdentifier nodeId, final long time, final TimeUnit timeUnit) {
ClusterUtils.waitUntilConditionMet(time, timeUnit,
() -> getClusterCoordinator().getConnectionStatus(nodeId).getState() == NodeConnectionState.DISCONNECTED,
() -> "Connection Status is " + getClusterCoordinator().getConnectionStatus(nodeId).toString());
}
/**
* Asserts that the node with the given ID is currently connected (According to this node!)
*
* @param nodeId id of the node
*/
public void assertNodeIsConnected(final NodeIdentifier nodeId) {
assertEquals(NodeConnectionState.CONNECTED, getClusterCoordinator().getConnectionStatus(nodeId).getState());
}
/**
* Assert that the node with the given ID is offloaded (according to this node!) within the given amount of time
*
* @param nodeId id of the node
* @param time how long to wait
* @param timeUnit unit of time provided by the 'time' argument
*/
public void assertNodeIsOffloaded(final NodeIdentifier nodeId, final long time, final TimeUnit timeUnit) {
ClusterUtils.waitUntilConditionMet(time, timeUnit,
() -> getClusterCoordinator().getConnectionStatus(nodeId).getState() == NodeConnectionState.OFFLOADED,
() -> "Connection Status is " + getClusterCoordinator().getConnectionStatus(nodeId).toString());
}
}

View File

@ -1,116 +0,0 @@
/*
* 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.cluster.integration;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.state.Scope;
import org.apache.nifi.components.state.StateMap;
import org.apache.nifi.components.state.StateProvider;
import org.apache.nifi.components.state.StateProviderInitializationContext;
import org.apache.nifi.controller.state.StandardStateMap;
public class NopStateProvider implements StateProvider {
private final String id = UUID.randomUUID().toString();
private final Map<String, Map<String, String>> componentStateMap = new HashMap<>();
@Override
public Collection<ValidationResult> validate(ValidationContext context) {
return Collections.emptyList();
}
@Override
public PropertyDescriptor getPropertyDescriptor(String name) {
return null;
}
@Override
public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {
}
@Override
public List<PropertyDescriptor> getPropertyDescriptors() {
return Collections.emptyList();
}
@Override
public String getIdentifier() {
return id;
}
@Override
public void initialize(StateProviderInitializationContext context) throws IOException {
}
@Override
public void shutdown() {
}
@Override
public synchronized void setState(Map<String, String> state, String componentId) throws IOException {
final Map<String, String> stateMap = componentStateMap.computeIfAbsent(componentId, compId -> new HashMap<String, String>());
stateMap.clear();
stateMap.putAll(state);
}
@Override
public synchronized StateMap getState(String componentId) throws IOException {
return new StandardStateMap(componentStateMap.computeIfAbsent(componentId, compId -> new HashMap<String, String>()), 0L);
}
@Override
public synchronized boolean replace(StateMap oldValue, Map<String, String> newValue, String componentId) throws IOException {
return false;
}
@Override
public void clear(String componentId) throws IOException {
}
@Override
public void onComponentRemoved(String componentId) throws IOException {
}
@Override
public void enable() {
}
@Override
public void disable() {
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Scope[] getSupportedScopes() {
return new Scope[] {Scope.LOCAL};
}
}

View File

@ -93,6 +93,7 @@ import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -116,9 +117,11 @@ import java.util.stream.Collectors;
* <p/>
*/
public class StandardProcessSession implements ProcessSession, ProvenanceEventEnricher {
private static final long VERSION_INCREMENT = 1;
private static final String INITIAL_VERSION = String.valueOf(VERSION_INCREMENT);
private static final AtomicLong idGenerator = new AtomicLong(0L);
private static final AtomicLong enqueuedIndex = new AtomicLong(0L);
private static final StateMap EMPTY_STATE_MAP = new StandardStateMap(Collections.emptyMap(), -1L);
private static final StateMap EMPTY_STATE_MAP = new StandardStateMap(Collections.emptyMap(), Optional.empty());
// determines how many things must be transferred, removed, modified in order to avoid logging the FlowFile ID's on commit/rollback
public static final int VERBOSE_LOG_THRESHOLD = 10;
@ -685,7 +688,8 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
final StateManager stateManager = context.getStateManager();
if (checkpoint.localState != null) {
final StateMap stateMap = stateManager.getState(Scope.LOCAL);
if (stateMap.getVersion() < checkpoint.localState.getVersion()) {
final Optional<String> stateVersion = stateMap.getStateVersion();
if (!stateVersion.equals(checkpoint.localState.getStateVersion())) {
LOG.debug("Updating State Manager's Local State");
try {
@ -695,14 +699,15 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
}
} else {
LOG.debug("Will not update State Manager's Local State because the State Manager reports the latest version as {}, which is newer than the session's known version of {}.",
stateMap.getVersion(), checkpoint.localState.getVersion());
stateVersion, checkpoint.localState.getStateVersion());
}
}
// Update cluster state
if (checkpoint.clusterState != null) {
final StateMap stateMap = stateManager.getState(Scope.CLUSTER);
if (stateMap.getVersion() < checkpoint.clusterState.getVersion()) {
final Optional<String> stateVersion = stateMap.getStateVersion();
if (!stateVersion.equals(checkpoint.clusterState.getStateVersion())) {
LOG.debug("Updating State Manager's Cluster State");
try {
@ -712,7 +717,7 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
}
} else {
LOG.debug("Will not update State Manager's Cluster State because the State Manager reports the latest version as {}, which is newer than the session's known version of {}.",
stateMap.getVersion(), checkpoint.clusterState.getVersion());
stateVersion, checkpoint.clusterState.getStateVersion());
}
}
@ -3816,8 +3821,9 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
@Override
public void setState(final Map<String, String> state, final Scope scope) throws IOException {
final long currentVersion = getState(scope).getVersion();
final StateMap stateMap = new StandardStateMap(state, currentVersion + 1);
final Optional<String> currentVersion = getState(scope).getStateVersion();
final String version = currentVersion.map(this::getIncrementedVersion).orElse(INITIAL_VERSION);
final StateMap stateMap = new StandardStateMap(state, Optional.of(version));
setState(stateMap, scope);
}
@ -3858,8 +3864,8 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
@Override
public boolean replaceState(final StateMap oldValue, final Map<String, String> newValue, final Scope scope) throws IOException {
final StateMap current = getState(scope);
if (current.getVersion() == -1 && (oldValue == null || oldValue.getVersion() == -1)) {
final StateMap stateMap = new StandardStateMap(newValue, 1L);
if (!current.getStateVersion().isPresent() && (oldValue == null || !oldValue.getStateVersion().isPresent())) {
final StateMap stateMap = new StandardStateMap(newValue, Optional.of(INITIAL_VERSION));
setState(stateMap, scope);
return true;
}
@ -3868,8 +3874,9 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
return false;
}
if (current.getVersion() == oldValue.getVersion() && current.toMap().equals(oldValue.toMap())) {
final StateMap stateMap = new StandardStateMap(newValue, current.getVersion() + 1);
if (current.getStateVersion().equals(oldValue.getStateVersion()) && current.toMap().equals(oldValue.toMap())) {
final String version = current.getStateVersion().map(this::getIncrementedVersion).orElse(INITIAL_VERSION);
final StateMap stateMap = new StandardStateMap(newValue, Optional.of(version));
setState(stateMap, scope);
return true;
}
@ -3887,6 +3894,12 @@ public class StandardProcessSession implements ProcessSession, ProvenanceEventEn
return "StandardProcessSession[id=" + sessionId + "]";
}
private String getIncrementedVersion(final String currentVersion) {
final long versionNumber = Long.parseLong(currentVersion);
final long version = versionNumber + VERSION_INCREMENT;
return String.valueOf(version);
}
/**
* Callback interface used to poll a FlowFileQueue, in order to perform
* functional programming-type of polling a queue

View File

@ -19,21 +19,30 @@ package org.apache.nifi.controller.state;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import org.apache.nifi.components.state.StateMap;
public class StandardStateMap implements StateMap {
private final Map<String, String> stateValues;
private final long version;
private static final int EMPTY_VERSION = -1;
public StandardStateMap(final Map<String, String> stateValues, final long version) {
this.stateValues = Collections.unmodifiableMap(stateValues == null ? Collections.<String, String> emptyMap() : stateValues);
this.version = version;
private final Map<String, String> stateValues;
private final Optional<String> stateVersion;
public StandardStateMap(final Map<String, String> stateValues, final Optional<String> stateVersion) {
this.stateValues = Collections.unmodifiableMap(stateValues == null ? Collections.emptyMap() : stateValues);
this.stateVersion = stateVersion;
}
@Override
public long getVersion() {
return version;
return stateVersion.map(version -> version.hashCode()).orElse(EMPTY_VERSION);
}
@Override
public Optional<String> getStateVersion() {
return stateVersion;
}
@Override
@ -48,6 +57,6 @@ public class StandardStateMap implements StateMap {
@Override
public String toString() {
return "StandardStateMap[version=" + version + ", values=" + stateValues + "]";
return "StandardStateMap[version=" + stateVersion + ", values=" + stateValues + "]";
}
}

View File

@ -283,7 +283,7 @@ public class StandardDataValve implements DataValve {
return;
}
if (stateMap.getVersion() < 0) {
if (!stateMap.getStateVersion().isPresent()) {
logger.debug("No state to recover for {}", this);
return;
}

View File

@ -52,6 +52,21 @@
<artifactId>nifi-framework-components</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-leader-election-shared</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-zookeeper-leader-election</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-cluster-zookeeper</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-expression-language</artifactId>

View File

@ -732,10 +732,10 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
// flow that is different from the rest of the cluster (especially an empty flow) and then
// kicking everyone out. This way, we instead inherit the cluster flow before we attempt to be
// the coordinator.
LOG.info("Checking if there is already a Cluster Coordinator Elected...");
final String clusterCoordinatorAddress = leaderElectionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
if (StringUtils.isEmpty(clusterCoordinatorAddress)) {
LOG.info("It appears that no Cluster Coordinator has been Elected yet. Registering for Cluster Coordinator Role.");
LOG.info("Checking for elected Cluster Coordinator...");
final Optional<String> clusterCoordinatorLeader = leaderElectionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
if (!clusterCoordinatorLeader.isPresent()) {
LOG.info("No Cluster Coordinator elected: Registering for Cluster Coordinator election");
registerForClusterCoordinator(true);
} else {
// At this point, we have determined that there is a Cluster Coordinator elected. It is important to note, though,
@ -746,8 +746,8 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
// to that address has not started. ZooKeeper/Curator will recognize this after a while and delete the ZNode. As a result,
// we may later determine that there is in fact no Cluster Coordinator. If this happens, we will automatically register for
// Cluster Coordinator through the StandardFlowService.
LOG.info("The Election for Cluster Coordinator has already begun (Leader is {}). Will not register to be elected for this role until after connecting "
+ "to the cluster and inheriting the cluster's flow.", clusterCoordinatorAddress);
LOG.info("Cluster Coordinator [{}] elected: Not registering for election until after connecting "
+ "to the cluster and inheriting the flow", clusterCoordinatorLeader.get());
registerForClusterCoordinator(false);
}
@ -2377,9 +2377,10 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
leaderElectionManager.register(ClusterRoles.CLUSTER_COORDINATOR, new LeaderElectionStateChangeListener() {
@Override
public synchronized void onLeaderRelinquish() {
LOG.info("This node is no longer the elected Active Cluster Coordinator");
bulletinRepository.addBulletin(BulletinFactory.createBulletin("Cluster Coordinator", Severity.INFO.name(), participantId + " is no longer the Cluster Coordinator"));
public synchronized void onStopLeading() {
LOG.info("This node is no longer the elected Active {}", ClusterRoles.CLUSTER_COORDINATOR);
final String message = String.format("%s is no longer the elected Active %s", participantId, ClusterRoles.CLUSTER_COORDINATOR);
bulletinRepository.addBulletin(BulletinFactory.createBulletin(ClusterRoles.CLUSTER_COORDINATOR, Severity.INFO.name(), message));
// We do not want to stop the heartbeat monitor. This is because even though ZooKeeper offers guarantees
// that watchers will see changes on a ZNode in the order they happened, there does not seem to be any
@ -2392,9 +2393,10 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
}
@Override
public synchronized void onLeaderElection() {
LOG.info("This node elected Active Cluster Coordinator");
bulletinRepository.addBulletin(BulletinFactory.createBulletin("Cluster Coordinator", Severity.INFO.name(), participantId + " has been elected the Cluster Coordinator"));
public synchronized void onStartLeading() {
LOG.info("This node has been elected Active {}", ClusterRoles.CLUSTER_COORDINATOR);
final String message = String.format("%s has been elected Active %s", participantId, ClusterRoles.CLUSTER_COORDINATOR);
bulletinRepository.addBulletin(BulletinFactory.createBulletin(ClusterRoles.CLUSTER_COORDINATOR, Severity.INFO.name(), message ));
// Purge any heartbeats that we already have. If we don't do this, we can have a scenario where we receive heartbeats
// from a node, and then another node becomes Cluster Coordinator. As a result, we stop receiving heartbeats. Now that
@ -2411,12 +2413,12 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
leaderElectionManager.register(ClusterRoles.PRIMARY_NODE, new LeaderElectionStateChangeListener() {
@Override
public void onLeaderElection() {
public void onStartLeading() {
setPrimary(true);
}
@Override
public void onLeaderRelinquish() {
public void onStopLeading() {
setPrimary(false);
}
}, participantId);
@ -2546,7 +2548,7 @@ public class FlowController implements ReportingTaskProvider, Authorizable, Node
// Emit a bulletin detailing the fact that the primary node state has changed
if (oldBean == null || oldBean.isPrimary() != primary) {
final String message = primary ? "This node has been elected Primary Node" : "This node is no longer Primary Node";
final Bulletin bulletin = BulletinFactory.createBulletin("Primary Node", Severity.INFO.name(), message);
final Bulletin bulletin = BulletinFactory.createBulletin(ClusterRoles.PRIMARY_NODE, Severity.INFO.name(), message);
bulletinRepository.addBulletin(bulletin);
LOG.info(message);
}

View File

@ -37,6 +37,7 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -62,12 +63,8 @@ public class ClusterProtocolHeartbeater implements Heartbeater {
@Override
public String getHeartbeatAddress() {
final String heartbeatAddress = electionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
if (heartbeatAddress == null) {
throw new ProtocolException("Cannot send heartbeat because there is no Cluster Coordinator currently elected");
}
return heartbeatAddress;
final Optional<String> clusterCoordinator = electionManager.getLeader(ClusterRoles.CLUSTER_COORDINATOR);
return clusterCoordinator.orElseThrow(() -> new ProtocolException("Unable to send heartbeat: Cluster Coordinator not found"));
}

View File

@ -19,6 +19,7 @@ package org.apache.nifi.controller.leader.election;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
@ -46,8 +47,8 @@ public class StandaloneLeaderElectionManager implements LeaderElectionManager {
}
@Override
public String getLeader(final String roleName) {
return null;
public Optional<String> getLeader(final String roleName) {
return Optional.empty();
}
@Override
@ -59,11 +60,6 @@ public class StandaloneLeaderElectionManager implements LeaderElectionManager {
return false;
}
@Override
public boolean isStopped() {
return false;
}
@Override
public void stop() {
}
@ -92,9 +88,4 @@ public class StandaloneLeaderElectionManager implements LeaderElectionManager {
public long getPollCount() {
return -1L;
}
@Override
public boolean isLeaderElected(String roleName) {
return false;
}
}

View File

@ -22,12 +22,15 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.nifi.components.state.StateMap;
import org.wali.SerDe;
import org.wali.UpdateType;
public class StateMapSerDe implements SerDe<StateMapUpdate> {
private static final long EMPTY_VERSION = -1;
private static final int VERSION = 0;
@Override
@ -44,7 +47,7 @@ public class StateMapSerDe implements SerDe<StateMapUpdate> {
}
final StateMap stateMap = record.getStateMap();
final long recordVersion = stateMap.getVersion();
final long recordVersion = stateMap.getStateVersion().map(Long::parseLong).orElse(EMPTY_VERSION);
out.writeLong(recordVersion);
final Map<String, String> map = stateMap.toMap();
@ -89,7 +92,8 @@ public class StateMapSerDe implements SerDe<StateMapUpdate> {
stateValues.put(key, value);
}
return new StateMapUpdate(new StandardStateMap(stateValues, recordVersion), componentId, updateType);
final String stateVersion = String.valueOf(recordVersion);
return new StateMapUpdate(new StandardStateMap(stateValues, Optional.of(stateVersion)), componentId, updateType);
}
@Override

View File

@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
@ -52,6 +53,8 @@ import org.wali.WriteAheadRepository;
* Provides state management for local (standalone) state, backed by a write-ahead log
*/
public class WriteAheadLocalStateProvider extends AbstractStateProvider {
private static final long EMPTY_VERSION = -1;
private static final Logger logger = LoggerFactory.getLogger(WriteAheadLocalStateProvider.class);
private volatile boolean alwaysSync;
@ -131,14 +134,15 @@ public class WriteAheadLocalStateProvider extends AbstractStateProvider {
writeAheadLog = new MinimalLockingWriteAheadLog<>(basePath.toPath(), numPartitions, serde, null);
final Collection<StateMapUpdate> updates = writeAheadLog.recoverRecords();
long maxRecordVersion = -1L;
long maxRecordVersion = EMPTY_VERSION;
for (final StateMapUpdate update : updates) {
if (update.getUpdateType() == UpdateType.DELETE) {
continue;
}
final long recordVersion = update.getStateMap().getVersion();
final Optional<String> stateVersion = update.getStateMap().getStateVersion();
final long recordVersion = stateVersion.map(Long::parseLong).orElse(EMPTY_VERSION);
if (recordVersion > maxRecordVersion) {
maxRecordVersion = recordVersion;
}
@ -180,7 +184,7 @@ public class WriteAheadLocalStateProvider extends AbstractStateProvider {
private ComponentProvider getProvider(final String componentId) {
ComponentProvider componentProvider = componentProviders.get(componentId);
if (componentProvider == null) {
final StateMap stateMap = new StandardStateMap(Collections.<String, String> emptyMap(), -1L);
final StateMap stateMap = new StandardStateMap(Collections.emptyMap(), Optional.empty());
componentProvider = new ComponentProvider(writeAheadLog, versionGenerator, componentId, stateMap, alwaysSync);
final ComponentProvider existingComponentProvider = componentProviders.putIfAbsent(componentId, componentProvider);
@ -248,14 +252,14 @@ public class WriteAheadLocalStateProvider extends AbstractStateProvider {
// repository at a time for a record with the same key. I.e., many threads can update the repository at once, as long as they
// are not updating the repository with records that have the same identifier.
public synchronized void setState(final Map<String, String> state) throws IOException {
stateMap = new StandardStateMap(state, versionGenerator.incrementAndGet());
stateMap = new StandardStateMap(state, Optional.of(getIncrementedVersion()));
final StateMapUpdate updateRecord = new StateMapUpdate(stateMap, componentId, UpdateType.UPDATE);
wal.update(Collections.singleton(updateRecord), alwaysSync);
}
// see above explanation as to why this method is synchronized.
public synchronized boolean replace(final StateMap oldValue, final Map<String, String> newValue) throws IOException {
if (stateMap.getVersion() == -1L) {
if (!stateMap.getStateVersion().isPresent()) {
// state has never been set so return false
return false;
}
@ -264,17 +268,21 @@ public class WriteAheadLocalStateProvider extends AbstractStateProvider {
return false;
}
stateMap = new StandardStateMap(new HashMap<>(newValue), versionGenerator.incrementAndGet());
stateMap = new StandardStateMap(new HashMap<>(newValue), Optional.of(getIncrementedVersion()));
final StateMapUpdate updateRecord = new StateMapUpdate(stateMap, componentId, UpdateType.UPDATE);
wal.update(Collections.singleton(updateRecord), alwaysSync);
return true;
}
public synchronized void clear() throws IOException {
stateMap = new StandardStateMap(null, versionGenerator.incrementAndGet());
stateMap = new StandardStateMap(null, Optional.of(getIncrementedVersion()));
final StateMapUpdate update = new StateMapUpdate(stateMap, componentId, UpdateType.UPDATE);
wal.update(Collections.singleton(update), alwaysSync);
}
private String getIncrementedVersion() {
return String.valueOf(versionGenerator.incrementAndGet());
}
}
private class CheckpointTask implements Runnable {

View File

@ -28,8 +28,8 @@ 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.cluster.ZooKeeperClientConfig;
import org.apache.nifi.controller.cluster.SecureClientZooKeeperFactory;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.SecureClientZooKeeperFactory;
import org.apache.nifi.controller.state.StandardStateMap;
import org.apache.nifi.controller.state.providers.AbstractStateProvider;
import org.apache.nifi.processor.util.StandardValidators;
@ -61,6 +61,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -72,6 +73,8 @@ import java.util.stream.Collectors;
* consistency across configuration interactions.
*/
public class ZooKeeperStateProvider extends AbstractStateProvider {
private static final int EMPTY_VERSION = -1;
private static final Logger logger = LoggerFactory.getLogger(ZooKeeperStateProvider.class);
private NiFiProperties nifiProperties;
@ -344,7 +347,8 @@ public class ZooKeeperStateProvider extends AbstractStateProvider {
stateValues.put(key, value);
}
return new StandardStateMap(stateValues, recordVersion);
final String stateVersion = String.valueOf(recordVersion);
return new StandardStateMap(stateValues, Optional.of(stateVersion));
}
}
@ -470,7 +474,7 @@ public class ZooKeeperStateProvider extends AbstractStateProvider {
} catch (final KeeperException ke) {
final Code exceptionCode = ke.code();
if (Code.NONODE == exceptionCode) {
return new StandardStateMap(null, -1L);
return new StandardStateMap(null, Optional.empty());
}
if (Code.SESSIONEXPIRED == exceptionCode) {
invalidateClient();
@ -488,8 +492,9 @@ public class ZooKeeperStateProvider extends AbstractStateProvider {
public boolean replace(final StateMap oldValue, final Map<String, String> newValue, final String componentId) throws IOException {
verifyEnabled();
final int version = oldValue.getStateVersion().map(Integer::parseInt).orElse(EMPTY_VERSION);
try {
setState(newValue, (int) oldValue.getVersion(), componentId, false);
setState(newValue, version, componentId, false);
return true;
} catch (final NoNodeException nne) {
return false;

View File

@ -17,7 +17,7 @@
package org.apache.nifi.controller.state.server;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.common.X509Util;
import org.apache.zookeeper.server.DatadirCleanupManager;

View File

@ -14,24 +14,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.spring;
import org.apache.nifi.controller.leader.election.CuratorLeaderElectionManager;
import org.apache.nifi.controller.leader.election.LeaderElectionManager;
import org.apache.nifi.controller.leader.election.StandaloneLeaderElectionManager;
import org.apache.nifi.nar.ExtensionDefinition;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.NarThreadContextClassLoader;
import org.apache.nifi.util.NiFiProperties;
import org.springframework.beans.factory.FactoryBean;
import java.util.Optional;
import java.util.Set;
import static org.apache.nifi.util.NiFiProperties.CLUSTER_LEADER_ELECTION_IMPLEMENTATION;
import static org.apache.nifi.util.NiFiProperties.DEFAULT_CLUSTER_LEADER_ELECTION_IMPLEMENTATION;
public class LeaderElectionManagerFactoryBean implements FactoryBean<LeaderElectionManager> {
private int numThreads;
private ExtensionManager extensionManager;
private NiFiProperties properties;
@Override
public LeaderElectionManager getObject() throws Exception {
final boolean isNode = properties.isNode();
if (isNode) {
return new CuratorLeaderElectionManager(numThreads, properties);
return loadClusterLeaderElectionManager();
} else {
return new StandaloneLeaderElectionManager();
}
@ -47,11 +55,29 @@ public class LeaderElectionManagerFactoryBean implements FactoryBean<LeaderElect
return true;
}
public void setNumThreads(final int numThreads) {
this.numThreads = numThreads;
public void setExtensionManager(final ExtensionManager extensionManager) {
this.extensionManager = extensionManager;
}
public void setProperties(final NiFiProperties properties) {
this.properties = properties;
}
private LeaderElectionManager loadClusterLeaderElectionManager() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
final String leaderElectionImplementation = properties.getProperty(CLUSTER_LEADER_ELECTION_IMPLEMENTATION, DEFAULT_CLUSTER_LEADER_ELECTION_IMPLEMENTATION);
final Set<ExtensionDefinition> extensions = extensionManager.getExtensions(LeaderElectionManager.class);
final Optional<ExtensionDefinition> extensionFound = extensions.stream()
.filter(extensionDefinition -> {
final String extensionClassName = extensionDefinition.getImplementationClassName();
return extensionClassName.equals(leaderElectionImplementation) || extensionClassName.endsWith(leaderElectionImplementation);
})
.findFirst();
final ExtensionDefinition extension = extensionFound.orElseThrow(() -> {
final String message = String.format("No Extensions Found for %s", LeaderElectionManager.class.getName());
return new IllegalStateException(message);
});
final String extensionImplementationClass = extension.getImplementationClassName();
return NarThreadContextClassLoader.createInstance(extensionManager, extensionImplementationClass, LeaderElectionManager.class, properties);
}
}

View File

@ -16,7 +16,7 @@
*/
package org.apache.nifi.cluster;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.util.NiFiProperties;
import org.junit.jupiter.api.Test;

View File

@ -22,7 +22,8 @@ import java.util.Map;
import org.apache.curator.framework.api.ACLProvider;
import org.apache.curator.framework.imps.DefaultACLProvider;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.leader.zookeeper.CuratorACLProviderFactory;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.data.ACL;
import org.junit.jupiter.api.BeforeEach;

View File

@ -2495,7 +2495,7 @@ public class StandardProcessSessionIT {
public void testStateRetrievedHasVersion() throws IOException {
StateMap retrieved = session.getState(Scope.LOCAL);
assertNotNull(retrieved);
assertEquals(-1, retrieved.getVersion());
assertFalse(retrieved.getStateVersion().isPresent());
assertEquals(1, stateManager.getRetrievalCount(Scope.LOCAL));
assertEquals(0, stateManager.getRetrievalCount(Scope.CLUSTER));
@ -2505,13 +2505,13 @@ public class StandardProcessSessionIT {
retrieved = session.getState(Scope.LOCAL);
assertNotNull(retrieved);
assertEquals(0, retrieved.getVersion());
assertTrue(retrieved.getStateVersion().isPresent());
assertEquals(Collections.singletonMap("abc", "123"), retrieved.toMap());
session.setState(Collections.singletonMap("abc", "222"), Scope.LOCAL);
retrieved = session.getState(Scope.LOCAL);
assertNotNull(retrieved);
assertEquals(1, retrieved.getVersion());
assertTrue(retrieved.getStateVersion().isPresent());
session.commit();
stateManager.assertStateEquals("abc", "222", Scope.LOCAL);
@ -2519,7 +2519,7 @@ public class StandardProcessSessionIT {
retrieved = session.getState(Scope.LOCAL);
assertNotNull(retrieved);
assertEquals(1, retrieved.getVersion());
assertTrue(retrieved.getStateVersion().isPresent());
}
@Test

View File

@ -69,6 +69,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@ -86,7 +87,7 @@ public class TestStandardControllerServiceProvider {
@Override
public StateManager getStateManager(final String componentId) {
final StateManager stateManager = Mockito.mock(StateManager.class);
final StateMap emptyStateMap = new StandardStateMap(Collections.emptyMap(), -1);
final StateMap emptyStateMap = new StandardStateMap(Collections.emptyMap(), Optional.empty());
try {
Mockito.when(stateManager.getState(any(Scope.class))).thenReturn(emptyStateMap);
} catch (IOException e) {

View File

@ -28,9 +28,11 @@ import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestStateMapSerDe {
@ -42,7 +44,9 @@ public class TestStateMapSerDe {
final Map<String, String> stateValues = new HashMap<>();
stateValues.put("abc", "xyz");
stateValues.put("cba", "zyx");
final StateMap stateMap = new StandardStateMap(stateValues, 3L);
String version = "3";
final StateMap stateMap = new StandardStateMap(stateValues, Optional.of(version));
final StateMapUpdate record = new StateMapUpdate(stateMap, componentId, UpdateType.CREATE);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -61,7 +65,9 @@ public class TestStateMapSerDe {
assertEquals(UpdateType.CREATE, update.getUpdateType());
final StateMap recoveredStateMap = update.getStateMap();
assertEquals(3L, recoveredStateMap.getVersion());
final Optional<String> stateVersion = recoveredStateMap.getStateVersion();
assertTrue(stateVersion.isPresent());
assertEquals(version, stateVersion.get());
assertEquals(stateValues, recoveredStateMap.toMap());
}
}

View File

@ -59,7 +59,7 @@ public abstract class AbstractTestStateProvider {
StateMap map = provider.getState(componentId);
assertNotNull(map);
assertEquals(-1, map.getVersion());
assertFalse(map.getStateVersion().isPresent());
assertNotNull(map.toMap());
assertTrue(map.toMap().isEmpty());
@ -67,7 +67,7 @@ public abstract class AbstractTestStateProvider {
map = provider.getState(componentId);
assertNotNull(map);
assertEquals(0, map.getVersion());
assertTrue(map.getStateVersion().isPresent());
assertEquals("value1", map.get(key));
assertEquals("value1", map.toMap().get(key));
@ -77,7 +77,7 @@ public abstract class AbstractTestStateProvider {
map = provider.getState(componentId);
assertEquals("value2", map.get(key));
assertEquals(1L, map.getVersion());
assertTrue(map.getStateVersion().isPresent());
}
@Test
@ -89,7 +89,7 @@ public abstract class AbstractTestStateProvider {
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals("value1", stateMap.get(key));
assertEquals(0, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
provider.setState(Collections.singletonMap(key, "intermediate value"), componentId);
@ -98,7 +98,7 @@ public abstract class AbstractTestStateProvider {
assertEquals(key, stateMap.toMap().keySet().iterator().next());
assertEquals(1, stateMap.toMap().size());
assertEquals("intermediate value", stateMap.get(key));
assertEquals(1, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
}
@ -116,13 +116,13 @@ public abstract class AbstractTestStateProvider {
assertEquals(1, map.size());
assertEquals("value", map.get(key));
provider.setState(Collections.<String, String> emptyMap(), componentId);
provider.setState(Collections.emptyMap(), componentId);
final StateMap stateMap = provider.getState(componentId);
map = stateMap.toMap();
assertNotNull(map);
assertTrue(map.isEmpty());
assertEquals(1, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
}
@Test
@ -130,21 +130,21 @@ public abstract class AbstractTestStateProvider {
final StateProvider provider = getProvider();
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(-1L, stateMap.getVersion());
assertFalse(stateMap.getStateVersion().isPresent());
assertTrue(stateMap.toMap().isEmpty());
provider.setState(Collections.singletonMap("testClear", "value"), componentId);
stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(0, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
assertEquals("value", stateMap.get("testClear"));
provider.clear(componentId);
stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(1L, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
assertTrue(stateMap.toMap().isEmpty());
}
@ -196,7 +196,7 @@ public abstract class AbstractTestStateProvider {
provider.setState(newValue, componentId);
final StateMap stateMap = provider.getState(componentId);
assertEquals(0L, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
provider.onComponentRemoved(componentId);
@ -205,8 +205,8 @@ public abstract class AbstractTestStateProvider {
final StateMap stateMapAfterRemoval = provider.getState(componentId);
// version should be -1 because the state has been removed entirely.
assertEquals(-1L, stateMapAfterRemoval.getVersion());
// version should not be present because the state has been removed entirely.
assertFalse(stateMapAfterRemoval.getStateVersion().isPresent());
}
protected abstract StateProvider getProvider();

View File

@ -1,277 +0,0 @@
/*
* 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 org.apache.commons.io.FileUtils;
import org.apache.curator.test.InstanceSpec;
import org.apache.nifi.attribute.expression.language.StandardPropertyValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.PropertyValue;
import org.apache.nifi.components.state.StateProvider;
import org.apache.nifi.components.state.StateProviderInitializationContext;
import org.apache.nifi.components.state.exception.StateTooLargeException;
import org.apache.nifi.controller.state.providers.AbstractTestStateProvider;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.mock.MockComponentLogger;
import org.apache.nifi.parameter.ParameterLookup;
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.server.ServerCnxnFactory;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.apache.nifi.leader.election.ITSecureClientZooKeeperFactory.createAndStartServer;
import static org.apache.nifi.leader.election.ITSecureClientZooKeeperFactory.createSecureClientProperties;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class ITZooKeeperStateProvider extends AbstractTestStateProvider {
private static final Logger logger = LoggerFactory.getLogger(ITZooKeeperStateProvider.class);
private volatile StateProvider provider;
private volatile ZooKeeperServer zkServer;
private final static Map<PropertyDescriptor, String> stateProviderProperties = new HashMap<>();
private static Path tempDir;
private static NiFiProperties nifiProperties;
private static TlsConfiguration tlsConfiguration;
@BeforeAll
public static void setTlsConfiguration() {
tlsConfiguration = new TemporaryKeyStoreBuilder().build();
}
@BeforeEach
public void setup() throws Exception {
tempDir = Paths.get("target/TestZooKeeperStateProvider");
final Path dataDir = tempDir.resolve("state");
final int clientPort = InstanceSpec.getRandomPort();
Files.createDirectory(tempDir);
// Set up the testing server
final ServerCnxnFactory serverConnectionFactory = createAndStartServer(
dataDir,
tempDir,
clientPort,
Paths.get(tlsConfiguration.getKeystorePath()),
tlsConfiguration.getKeystorePassword(),
Paths.get(tlsConfiguration.getTruststorePath()),
tlsConfiguration.getTruststorePassword()
);
zkServer = serverConnectionFactory.getZooKeeperServer();
// Set up state provider (client) TLS properties, normally injected through StateProviderContext annotation
nifiProperties = createSecureClientProperties(
clientPort,
Paths.get(tlsConfiguration.getKeystorePath()),
tlsConfiguration.getKeystoreType().getType(),
tlsConfiguration.getKeystorePassword(),
Paths.get(tlsConfiguration.getTruststorePath()),
tlsConfiguration.getTruststoreType().getType(),
tlsConfiguration.getTruststorePassword()
);
// Set up state provider properties
stateProviderProperties.put(ZooKeeperStateProvider.SESSION_TIMEOUT, "15 secs");
stateProviderProperties.put(ZooKeeperStateProvider.ROOT_NODE, "/nifi/team1/testing");
stateProviderProperties.put(ZooKeeperStateProvider.ACCESS_CONTROL, ZooKeeperStateProvider.OPEN_TO_WORLD.getValue());
final Map<PropertyDescriptor, String> properties = new HashMap<>(stateProviderProperties);
properties.put(ZooKeeperStateProvider.CONNECTION_STRING, "localhost:".concat(String.valueOf(clientPort)));
this.provider = createProvider(properties);
}
private void initializeProvider(final ZooKeeperStateProvider provider, final Map<PropertyDescriptor, String> properties) throws IOException {
provider.setNiFiProperties(nifiProperties);
provider.initialize(new StateProviderInitializationContext() {
@Override
public String getIdentifier() {
return "Unit Test Provider Initialization Context";
}
@Override
public Map<PropertyDescriptor, PropertyValue> getProperties() {
final Map<PropertyDescriptor, PropertyValue> propValueMap = new HashMap<>();
for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) {
propValueMap.put(entry.getKey(), new StandardPropertyValue(entry.getValue(), null, ParameterLookup.EMPTY));
}
return propValueMap;
}
@Override
public Map<String,String> getAllProperties() {
final Map<String,String> propValueMap = new LinkedHashMap<>();
for (final Map.Entry<PropertyDescriptor, PropertyValue> entry : getProperties().entrySet()) {
propValueMap.put(entry.getKey().getName(), entry.getValue().getValue());
}
propValueMap.put(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, Boolean.TRUE.toString());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE, tlsConfiguration.getKeystorePath());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE, tlsConfiguration.getTruststorePath());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword());
propValueMap.put(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType());
return propValueMap;
}
@Override
public PropertyValue getProperty(final PropertyDescriptor property) {
final String prop = properties.get(property);
return new StandardPropertyValue(prop, null, ParameterLookup.EMPTY);
}
// This won't be used by the ZooKeeper State Provider. I don't believe there's a way to pass an SSLContext
// directly to ZooKeeper anyway.
@Override
public SSLContext getSSLContext() {
return null;
}
@Override
public ComponentLog getLogger() {
return new MockComponentLogger();
}
});
}
private ZooKeeperStateProvider createProvider(final Map<PropertyDescriptor, String> properties) throws Exception {
final ZooKeeperStateProvider provider = new ZooKeeperStateProvider();
initializeProvider(provider, properties);
provider.enable();
return provider;
}
@AfterEach
public void clear() throws IOException {
try {
if (provider != null) {
provider.onComponentRemoved(componentId);
provider.disable();
provider.shutdown();
}
} finally {
if (zkServer != null) {
zkServer.shutdown(true);
clearDirectories();
}
}
}
private static void clearDirectories() {
try {
FileUtils.deleteDirectory(new File(tempDir.toString()));
} catch (IOException e) {
logger.error("Failed to delete: " + tempDir.toString(), e);
}
}
@Override
protected StateProvider getProvider() {
return provider;
}
@Timeout(30)
@Test
public void testStateTooLargeExceptionThrownOnSetState() throws InterruptedException {
final Map<String, String> state = new HashMap<>();
final StringBuilder sb = new StringBuilder();
// Build a string that is a little less than 64 KB, because that's
// the largest value available for DataOutputStream.writeUTF
for (int i = 0; i < 6500; i++) {
sb.append("0123456789");
}
for (int i = 0; i < 20; i++) {
state.put("numbers." + i, sb.toString());
}
while (true) {
try {
getProvider().setState(state, componentId);
fail("Expected StateTooLargeException");
} catch (final StateTooLargeException stle) {
// expected behavior.
break;
} catch (final IOException ioe) {
// If we attempt to interact with the server too quickly, we will get a
// ZooKeeper ConnectionLoss Exception, which the provider wraps in an IOException.
// We will wait 1 second in this case and try again. The test will timeout if this
// does not succeeed within 30 seconds.
Thread.sleep(1000L);
} catch (final Exception e) {
logger.error("Something went wrong attempting to set the state in testStateTooLargeExceptionThrownOnSetState()");
fail("Expected StateTooLargeException but " + e.getClass() + " was thrown", e);
}
}
}
@Timeout(30)
@Test
public void testStateTooLargeExceptionThrownOnReplace() throws InterruptedException {
final Map<String, String> state = new HashMap<>();
final StringBuilder sb = new StringBuilder();
// Build a string that is a little less than 64 KB, because that's
// the largest value available for DataOutputStream.writeUTF
for (int i = 0; i < 6500; i++) {
sb.append("0123456789");
}
for (int i = 0; i < 20; i++) {
state.put("numbers." + i, sb.toString());
}
final Map<String, String> smallState = new HashMap<>();
smallState.put("abc", "xyz");
while (true) {
try {
getProvider().setState(smallState, componentId);
break;
} catch (final IOException ioe) {
// If we attempt to interact with the server too quickly, we will get a
// ZooKeeper ConnectionLoss Exception, which the provider wraps in an IOException.
// We will wait 1 second in this case and try again. The test will timeout if this
// does not succeeed within 30 seconds.
Thread.sleep(1000L);
}
}
assertThrows(StateTooLargeException.class, () -> getProvider().replace(getProvider().getState(componentId), state, componentId));
}
}

View File

@ -22,8 +22,8 @@ import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryOneTime;
import org.apache.curator.utils.DefaultZookeeperFactory;
import org.apache.curator.utils.ZookeeperFactory;
import org.apache.nifi.controller.cluster.SecureClientZooKeeperFactory;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.SecureClientZooKeeperFactory;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
import org.apache.nifi.security.util.TlsConfiguration;
import org.apache.nifi.util.NiFiProperties;

View File

@ -40,7 +40,7 @@ import org.apache.nifi.controller.ProcessorNode;
import org.apache.nifi.controller.StandardSnippet;
import org.apache.nifi.controller.XmlFlowSynchronizer;
import org.apache.nifi.controller.flow.StandardFlowManager;
import org.apache.nifi.controller.leader.election.CuratorLeaderElectionManager;
import org.apache.nifi.framework.cluster.leader.zookeeper.CuratorLeaderElectionManager;
import org.apache.nifi.controller.leader.election.LeaderElectionManager;
import org.apache.nifi.controller.queue.ConnectionEventListener;
import org.apache.nifi.controller.queue.FlowFileQueue;
@ -240,7 +240,8 @@ public class FrameworkIntegrationTest {
clusterCoordinator = Mockito.mock(ClusterCoordinator.class);
final HeartbeatMonitor heartbeatMonitor = Mockito.mock(HeartbeatMonitor.class);
final NodeProtocolSender protocolSender = Mockito.mock(NodeProtocolSender.class);
final LeaderElectionManager leaderElectionManager = new CuratorLeaderElectionManager(2, nifiProperties);
final LeaderElectionManager leaderElectionManager = new CuratorLeaderElectionManager(nifiProperties);
final NodeIdentifier localNodeId = new NodeIdentifier(UUID.randomUUID().toString(), "localhost", 8111, "localhost", 8081,
"localhost", 8082, "localhost", 8083, 8084, false, Collections.emptySet());

View File

@ -20,8 +20,8 @@ import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryOneTime;
import org.apache.curator.test.InstanceSpec;
import org.apache.nifi.controller.cluster.SecureClientZooKeeperFactory;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.SecureClientZooKeeperFactory;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.security.util.CertificateUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.zookeeper.common.ClientX509Util;

View File

@ -0,0 +1,85 @@
/*
* 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.spring;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.controller.leader.election.LeaderElectionManager;
import org.apache.nifi.controller.leader.election.StandaloneLeaderElectionManager;
import org.apache.nifi.nar.ExtensionDefinition;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.util.NiFiProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LeaderElectionManagerFactoryBeanTest {
@Mock
ExtensionManager extensionManager;
@Mock
Bundle bundle;
LeaderElectionManagerFactoryBean bean;
@BeforeEach
void setBean() {
bean = new LeaderElectionManagerFactoryBean();
bean.setExtensionManager(extensionManager);
}
@Test
void testGetObjectStandalone() throws Exception {
final NiFiProperties properties = NiFiProperties.createBasicNiFiProperties(null, Collections.emptyMap());
bean.setProperties(properties);
final LeaderElectionManager leaderElectionManager = bean.getObject();
assertInstanceOf(StandaloneLeaderElectionManager.class, leaderElectionManager);
}
@Test
void testGetObjectCluster() throws Exception {
final Map<String, String> clusterProperties = new LinkedHashMap<>();
clusterProperties.put(NiFiProperties.CLUSTER_IS_NODE, Boolean.TRUE.toString());
clusterProperties.put(NiFiProperties.CLUSTER_LEADER_ELECTION_IMPLEMENTATION, MockLeaderElectionManager.class.getSimpleName());
final NiFiProperties properties = NiFiProperties.createBasicNiFiProperties(null, clusterProperties);
bean.setProperties(properties);
when(bundle.getClassLoader()).thenReturn(Thread.currentThread().getContextClassLoader());
when(extensionManager.getBundles(eq(MockLeaderElectionManager.class.getName()))).thenReturn(Collections.singletonList(bundle));
final ExtensionDefinition extension = new ExtensionDefinition(MockLeaderElectionManager.class.getName(), bundle, LeaderElectionManager.class);
when(extensionManager.getExtensions(eq(LeaderElectionManager.class))).thenReturn(Collections.singleton(extension));
final LeaderElectionManager leaderElectionManager = bean.getObject();
assertInstanceOf(MockLeaderElectionManager.class, leaderElectionManager);
}
}

View File

@ -0,0 +1,22 @@
/*
* 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.spring;
import org.apache.nifi.controller.leader.election.StandaloneLeaderElectionManager;
public class MockLeaderElectionManager extends StandaloneLeaderElectionManager {
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-bundle</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-kubernetes-leader-election</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-leader-election-shared</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-kubernetes-client</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,316 @@
/*
* 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.kubernetes.leader.election;
import org.apache.nifi.controller.leader.election.LeaderElectionRole;
import org.apache.nifi.controller.leader.election.LeaderElectionStateChangeListener;
import org.apache.nifi.controller.leader.election.TrackedLeaderElectionManager;
import org.apache.nifi.kubernetes.client.KubernetesClientProvider;
import org.apache.nifi.kubernetes.client.NamespaceProvider;
import org.apache.nifi.kubernetes.client.ServiceAccountNamespaceProvider;
import org.apache.nifi.kubernetes.client.StandardKubernetesClientProvider;
import org.apache.nifi.kubernetes.leader.election.command.LeaderElectionCommandProvider;
import org.apache.nifi.kubernetes.leader.election.command.StandardLeaderElectionCommandProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Kubernetes Leader Election Manager implementation using Kubernetes Lease Resources
*/
public class KubernetesLeaderElectionManager extends TrackedLeaderElectionManager {
private static final boolean INTERRUPT_ENABLED = true;
private static final int SERVICE_THREADS = 4;
private static final Logger logger = LoggerFactory.getLogger(KubernetesLeaderElectionManager.class);
private static final Map<String, String> ROLE_NAMES;
static {
final Map<String, String> roleNames = new LinkedHashMap<>();
for (final LeaderElectionRole leaderElectionRole : LeaderElectionRole.values()) {
roleNames.put(leaderElectionRole.getRoleName(), leaderElectionRole.getRoleId());
}
ROLE_NAMES = Collections.unmodifiableMap(roleNames);
}
private final ExecutorService executorService;
private final AtomicBoolean started = new AtomicBoolean();
private final Map<String, Future<?>> roleCommands = new ConcurrentHashMap<>();
private final Map<String, ParticipantRegistration> roleRegistrations = new ConcurrentHashMap<>();
private final Map<String, String> roleLeaders = new ConcurrentHashMap<>();
private final LeaderElectionCommandProvider leaderElectionCommandProvider;
/**
* Kubernetes Leader Election Manager default constructor
*/
public KubernetesLeaderElectionManager() {
executorService = createExecutorService();
leaderElectionCommandProvider = createLeaderElectionCommandProvider();
}
/**
* Start Manager and register current roles
*/
@Override
public void start() {
if (started.get()) {
logger.debug("Start requested when running");
} else {
started.getAndSet(true);
logger.debug("Started");
for (final ParticipantRegistration roleRegistration : roleRegistrations.values()) {
register(roleRegistration.roleName, roleRegistration.listener, roleRegistration.participantId);
}
}
}
/**
* Stop Manager and shutdown running commands
*/
@Override
public void stop() {
try {
leaderElectionCommandProvider.close();
} catch (final IOException e) {
logger.warn("Leader Election Command Factory close failed", e);
}
roleLeaders.clear();
executorService.shutdown();
started.getAndSet(false);
logger.debug("Stopped");
}
/**
* Register for Election or Observation based on presence of Participant ID and register for Leader when started
*
* @param roleName Role Name for registration
* @param listener State Change Listener for Leader Events
* @param participantId Participant ID or null when registering for Observation
*/
@Override
public synchronized void register(final String roleName, final LeaderElectionStateChangeListener listener, final String participantId) {
requireRoleName(roleName);
Objects.requireNonNull(listener, "Change Listener required");
final ParticipantRegistration roleRegistration = new ParticipantRegistration(roleName, participantId, listener);
roleRegistrations.put(roleName, roleRegistration);
final boolean participating = isParticipating(participantId);
if (participating) {
logger.debug("Registered Participation for Election Role [{}] ID [{}]", roleName, participantId);
if (started.get()) {
registerLeaderElectionCommand(roleName, listener, participantId);
}
} else {
logger.info("Registered Observation for Election Role [{}]", roleName);
}
}
/**
* Unregister for Leader Election of specified Role and cancel running command
*
* @param roleName Role Name to be removed from registration
*/
@Override
public synchronized void unregister(final String roleName) {
requireRoleName(roleName);
roleLeaders.remove(roleName);
final ParticipantRegistration roleRegistration = roleRegistrations.remove(roleName);
if (roleRegistration == null) {
logger.info("Not registered for Election Role [{}]", roleName);
} else {
final Future<?> roleCommand = roleCommands.remove(roleName);
if (roleCommand == null) {
logger.warn("Leader Election Command not found Role [{}] ID [{}]", roleName, roleRegistration.participantId);
} else {
roleCommand.cancel(INTERRUPT_ENABLED);
}
logger.info("Unregistered for Election Role [{}] ID [{}]", roleName, roleRegistration.participantId);
}
}
/**
* Determine whether current node is participating in Leader Election for specified Role
*
* @param roleName Role Name to be evaluated
* @return Participation status in Leader Election
*/
@Override
public boolean isActiveParticipant(final String roleName) {
requireRoleName(roleName);
final String participantId = getParticipantId(roleName);
return isParticipating(participantId);
}
/**
* Get Leader Identifier for Role
*
* @param roleName Role Name for requested Leader Identifier
* @return Leader Identifier or empty when not found
*/
@Override
public Optional<String> getLeader(final String roleName) {
requireRoleName(roleName);
final String roleId = getRoleId(roleName);
final long pollStarted = System.nanoTime();
try {
final Optional<String> leader = leaderElectionCommandProvider.findLeader(roleId);
leader.ifPresent(leaderId -> setRoleLeader(roleName, leaderId));
return leader;
} finally {
final long elapsed = System.nanoTime() - pollStarted;
registerPollTime(elapsed);
}
}
/**
* Determine whether current node is the Leader for the specified Role
*
* @param roleName Role Name to be evaluated
* @return Leader Status
*/
@Override
public boolean isLeader(final String roleName) {
requireRoleName(roleName);
final boolean leader;
final String participantId = getParticipantId(roleName);
if (participantId == null) {
logger.debug("Role [{}] not participating in Leader election", roleName);
leader = false;
} else {
final Optional<String> leaderAddress = getLeader(roleName);
final String leaderId = leaderAddress.orElse(null);
leader = participantId.equals(leaderId);
if (leader) {
logger.debug("Role [{}] Participant ID [{}] is Leader", roleName, participantId);
} else {
logger.debug("Role [{}] Participant ID [{}] not Leader", roleName, leaderId);
}
}
return leader;
}
protected ExecutorService createExecutorService() {
return Executors.newFixedThreadPool(SERVICE_THREADS, new NamedThreadFactory());
}
protected LeaderElectionCommandProvider createLeaderElectionCommandProvider() {
final NamespaceProvider namespaceProvider = new ServiceAccountNamespaceProvider();
final String namespace = namespaceProvider.getNamespace();
final KubernetesClientProvider kubernetesClientProvider = new StandardKubernetesClientProvider();
return new StandardLeaderElectionCommandProvider(kubernetesClientProvider, namespace);
}
private synchronized void registerLeaderElectionCommand(final String roleName, final LeaderElectionStateChangeListener listener, final String participantId) {
final Future<?> currentRoleCommand = roleCommands.get(roleName);
if (currentRoleCommand == null) {
final String roleId = getRoleId(roleName);
final Runnable leaderElectionCommand = leaderElectionCommandProvider.getCommand(
roleId,
participantId,
listener::onStartLeading,
listener::onStopLeading,
leaderId -> setRoleLeader(roleName, leaderId)
);
final Future<?> roleCommand = executorService.submit(leaderElectionCommand);
roleCommands.put(roleName, roleCommand);
logger.info("Registered command for Election Role [{}] ID [{}]", roleName, participantId);
}
}
private void setRoleLeader(final String roleName, final String leaderId) {
final String previousLeaderId = roleLeaders.put(roleName, leaderId);
if (leaderId.equals(previousLeaderId)) {
logger.debug("Role [{}] Leader [{}] not changed", roleName, leaderId);
} else {
logger.debug("Role [{}] Leader [{}] Previous [{}] changed", roleName, leaderId, previousLeaderId);
onLeaderChanged(roleName);
}
}
private String getParticipantId(final String roleName) {
final ParticipantRegistration roleRegistration = roleRegistrations.get(roleName);
return roleRegistration == null ? null : roleRegistration.participantId;
}
private void requireRoleName(final String roleName) {
if (roleName == null || roleName.isEmpty()) {
throw new IllegalArgumentException("Role Name required");
}
}
private String getRoleId(final String roleName) {
final String roleId = ROLE_NAMES.get(roleName);
if (roleId == null) {
throw new IllegalArgumentException(String.format("Role Name [%s] not supported", roleName));
}
return roleId;
}
private static class ParticipantRegistration {
private final String roleName;
private final String participantId;
private final LeaderElectionStateChangeListener listener;
private ParticipantRegistration(final String roleName, final String participantId, final LeaderElectionStateChangeListener listener) {
this.roleName = roleName;
this.participantId = participantId;
this.listener = listener;
}
}
private static class NamedThreadFactory implements ThreadFactory {
private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(final Runnable runnable) {
final Thread thread = defaultFactory.newThread(runnable);
thread.setName(KubernetesLeaderElectionManager.class.getSimpleName());
thread.setDaemon(true);
return thread;
}
}
}

View File

@ -0,0 +1,104 @@
/*
* 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.kubernetes.leader.election.command;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderCallbacks;
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfig;
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElectionConfigBuilder;
import io.fabric8.kubernetes.client.extended.leaderelection.LeaderElector;
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.LeaseLock;
import io.fabric8.kubernetes.client.extended.leaderelection.resourcelock.Lock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Runnable command for starting the Leader Election process
*/
class LeaderElectionCommand implements Runnable {
/** Leader Lease Duration based on kube-scheduler default setting */
private static final Duration LEASE_DURATION = Duration.ofSeconds(15);
/** Leader Lease Renew Deadline less than Lease Duration based on kube-scheduler default setting */
private static final Duration RENEW_DEADLINE = Duration.ofSeconds(10);
/** Lease Retry Period based on kube-scheduler default setting */
private static final Duration RETRY_PERIOD = Duration.ofSeconds(2);
private static final Logger logger = LoggerFactory.getLogger(LeaderElectionCommand.class);
private final KubernetesClient kubernetesClient;
private final LeaderCallbacks leaderCallbacks;
private final String name;
private final Lock lock;
LeaderElectionCommand(
final KubernetesClient kubernetesClient,
final String namespace,
final String name,
final String identity,
final Runnable onStartLeading,
final Runnable onStopLeading,
final Consumer<String> onNewLeader
) {
this.kubernetesClient = kubernetesClient;
this.name = Objects.requireNonNull(name, "Name required");
this.lock = new LeaseLock(namespace, name, identity);
this.leaderCallbacks = new LeaderCallbacks(onStartLeading, onStopLeading, onNewLeader);
}
@Override
public void run() {
logger.info("Election Name [{}] ID [{}] Participation STARTED", name, lock.identity());
while (!Thread.currentThread().isInterrupted()) {
runLeaderElector();
}
logger.info("Election Name [{}] ID [{}] Participation STOPPED", name, lock.identity());
}
private void runLeaderElector() {
logger.info("Election Name [{}] ID [{}] Command STARTED", name, lock.identity());
try {
final LeaderElectionConfig leaderElectionConfig = getLeaderElectionConfig();
final LeaderElector leaderElector = kubernetesClient.leaderElector().withConfig(leaderElectionConfig).build();
leaderElector.run();
logger.info("Election Name [{}] ID [{}] Command STOPPED", name, lock.identity());
} catch (final RuntimeException e) {
logger.error("Election Name [{}] ID [{}] Command FAILED", name, lock.identity(), e);
}
}
private LeaderElectionConfig getLeaderElectionConfig() {
return new LeaderElectionConfigBuilder()
.withName(name)
.withLeaderCallbacks(leaderCallbacks)
.withLock(lock)
.withLeaseDuration(LEASE_DURATION)
.withRenewDeadline(RENEW_DEADLINE)
.withRetryPeriod(RETRY_PERIOD)
.build();
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.kubernetes.leader.election.command;
import java.io.Closeable;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Provider abstraction for Kubernetes Leader Election Commands with callbacks
*/
public interface LeaderElectionCommandProvider extends Closeable {
/**
* Get Command with required properties
*
* @param name Election Name
* @param identity Election Participant Identity
* @param onStartLeading Callback run when elected as leader
* @param onStopLeading Callback run when no longer elected as leader
* @param onNewLeader Callback run with identification of new leader
* @return Runnable Command
*/
Runnable getCommand(
String name,
String identity,
Runnable onStartLeading,
Runnable onStopLeading,
Consumer<String> onNewLeader
);
/**
* Find Leader Identifier for specified Election Name
*
* @param name Election Name
* @return Leader Identifier or empty when not found
*/
Optional<String> findLeader(String name);
}

View File

@ -0,0 +1,130 @@
/*
* 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.kubernetes.leader.election.command;
import io.fabric8.kubernetes.api.model.coordination.v1.Lease;
import io.fabric8.kubernetes.api.model.coordination.v1.LeaseSpec;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import org.apache.nifi.kubernetes.client.KubernetesClientProvider;
import java.net.HttpURLConnection;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Standard implementation of Leader Election Command Provider with configurable namespace property
*/
public class StandardLeaderElectionCommandProvider implements LeaderElectionCommandProvider {
private final KubernetesClient kubernetesClient;
private final String namespace;
public StandardLeaderElectionCommandProvider(final KubernetesClientProvider kubernetesClientProvider, final String namespace) {
this.kubernetesClient = Objects.requireNonNull(kubernetesClientProvider).getKubernetesClient();
this.namespace = Objects.requireNonNull(namespace);
}
/**
* Get Leader Election Command with configured namespace and client provider
*
* @param name Election Name
* @param identity Election Participant Identity
* @param onStartLeading Callback run when elected as leader
* @param onStopLeading Callback run when no longer elected as leader
* @param onNewLeader Callback run with identification of new leader
* @return Leader Election Command
*/
@Override
public Runnable getCommand(
final String name,
final String identity,
final Runnable onStartLeading,
final Runnable onStopLeading,
final Consumer<String> onNewLeader
) {
return new LeaderElectionCommand(
kubernetesClient,
namespace,
name,
identity,
onStartLeading,
onStopLeading,
onNewLeader
);
}
/**
* Find Leader Identifier for specified Election Name
*
* @param name Election Name
* @return Leader Identifier or empty when not found or lease expired
*/
@Override
public Optional<String> findLeader(final String name) {
try {
final Lease lease = kubernetesClient.leases().inNamespace(namespace).withName(name).get();
final String currentHolderIdentity = getCurrentHolderIdentity(lease);
return Optional.ofNullable(currentHolderIdentity);
} catch (final KubernetesClientException e) {
if (isNotFound(e)) {
return Optional.empty();
} else {
throw e;
}
}
}
/**
* Close Kubernetes Client
*/
@Override
public void close() {
kubernetesClient.close();
}
private boolean isNotFound(final KubernetesClientException e) {
return HttpURLConnection.HTTP_NOT_FOUND == e.getCode();
}
private String getCurrentHolderIdentity(final Lease lease) {
final String holderIdentity;
if (lease == null) {
holderIdentity = null;
} else {
final LeaseSpec spec = lease.getSpec();
final ZonedDateTime expiration = getExpiration(spec);
final ZonedDateTime now = ZonedDateTime.now();
if (now.isAfter(expiration)) {
holderIdentity = null;
} else {
holderIdentity = spec.getHolderIdentity();
}
}
return holderIdentity;
}
private ZonedDateTime getExpiration(final LeaseSpec leaseSpec) {
final ZonedDateTime renewTime = leaseSpec.getRenewTime();
final Integer leaseDuration = leaseSpec.getLeaseDurationSeconds();
return renewTime.plusSeconds(leaseDuration);
}
}

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.kubernetes.leader.election.KubernetesLeaderElectionManager

View File

@ -0,0 +1,286 @@
/*
* 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.kubernetes.leader.election;
import org.apache.nifi.controller.leader.election.LeaderElectionRole;
import org.apache.nifi.controller.leader.election.LeaderElectionStateChangeListener;
import org.apache.nifi.kubernetes.leader.election.command.LeaderElectionCommandProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class KubernetesLeaderElectionManagerTest {
private static final LeaderElectionRole LEADER_ELECTION_ROLE = LeaderElectionRole.CLUSTER_COORDINATOR;
private static final String ROLE = LEADER_ELECTION_ROLE.getRoleName();
private static final String PARTICIPANT_ID = "Node-0";
@Mock
LeaderElectionStateChangeListener changeListener;
@Mock
ExecutorService executorService;
@Mock
Future<?> future;
@Captor
ArgumentCaptor<Runnable> commandCaptor;
ManagedLeaderElectionCommandProvider leaderElectionCommandProvider;
KubernetesLeaderElectionManager manager;
@BeforeEach
void setManager() {
leaderElectionCommandProvider = new ManagedLeaderElectionCommandProvider();
manager = new MockKubernetesLeaderElectionManager();
}
@Test
void testStartStartStop() {
manager.start();
manager.start();
manager.stop();
assertTrue(leaderElectionCommandProvider.closed);
}
@Test
void testStartIsLeaderFalseStop() {
manager.start();
final boolean leader = manager.isLeader(ROLE);
assertFalse(leader);
manager.stop();
assertTrue(leaderElectionCommandProvider.closed);
}
@Test
void testStartRegisterParticipatingStartLeading() {
manager.start();
setSubmitStartLeading();
manager.register(ROLE, changeListener, PARTICIPANT_ID);
captureRunCommand();
assertActiveParticipantLeader();
}
@Test
void testRegisterParticipatingStartLeading() {
manager.register(ROLE, changeListener, PARTICIPANT_ID);
setSubmitStartLeading();
manager.start();
captureRunCommand();
assertActiveParticipantLeader();
}
@Test
void testRegisterParticipatingStartLeadingUnregister() {
manager.register(ROLE, changeListener, PARTICIPANT_ID);
setSubmitStartLeading();
manager.start();
captureRunCommand();
assertActiveParticipantLeader();
manager.unregister(ROLE);
leaderElectionCommandProvider.leader = null;
assertNotActiveParticipantNotLeader();
assertEquals(LEADER_ELECTION_ROLE.getRoleId(), leaderElectionCommandProvider.findLeaderName);
}
@Test
void testIsLeaderNotRegistered() {
final boolean leader = manager.isLeader(ROLE);
assertFalse(leader);
}
@Test
void testRegisterParticipatingIsActiveParticipantTrue() {
manager.register(ROLE, changeListener, PARTICIPANT_ID);
final boolean activeParticipant = manager.isActiveParticipant(ROLE);
assertTrue(activeParticipant);
}
@Test
void testRegisterParticipatingIsActiveParticipantTrueUnregister() {
manager.register(ROLE, changeListener, PARTICIPANT_ID);
final boolean registeredActiveParticipant = manager.isActiveParticipant(ROLE);
assertTrue(registeredActiveParticipant);
manager.unregister(ROLE);
final boolean unregisteredActiveParticipant = manager.isActiveParticipant(ROLE);
assertFalse(unregisteredActiveParticipant);
}
@Test
void testRegisterNotParticipatingIsActiveParticipantFalse() {
manager.register(ROLE, changeListener);
final boolean activeParticipant = manager.isActiveParticipant(ROLE);
assertFalse(activeParticipant);
}
@Test
void testUnregisterRoleNameRequired() {
assertThrows(IllegalArgumentException.class, () -> manager.unregister(null));
}
@Test
void testUnregisterNotRegistered() {
manager.unregister(ROLE);
final boolean unregisteredActiveParticipant = manager.isActiveParticipant(ROLE);
assertFalse(unregisteredActiveParticipant);
}
private void setSubmitStartLeading() {
doReturn(future).when(executorService).submit(isA(Runnable.class));
leaderElectionCommandProvider.runStartLeading = true;
leaderElectionCommandProvider.runNewLeader = true;
leaderElectionCommandProvider.runStopLeading = true;
leaderElectionCommandProvider.leader = PARTICIPANT_ID;
}
private void captureRunCommand() {
verify(executorService).submit(commandCaptor.capture());
commandCaptor.getValue().run();
}
private void assertActiveParticipantLeader() {
final boolean activeParticipant = manager.isActiveParticipant(ROLE);
assertTrue(activeParticipant);
final boolean leader = manager.isLeader(ROLE);
assertTrue(leader);
final Optional<String> leaderId = manager.getLeader(ROLE);
assertTrue(leaderId.isPresent());
assertEquals(PARTICIPANT_ID, leaderId.get());
assertEquals(LEADER_ELECTION_ROLE.getRoleId(), leaderElectionCommandProvider.name);
}
private void assertNotActiveParticipantNotLeader() {
final boolean activeParticipant = manager.isActiveParticipant(ROLE);
assertFalse(activeParticipant);
final boolean leader = manager.isLeader(ROLE);
assertFalse(leader);
final Optional<String> leaderId = manager.getLeader(ROLE);
assertFalse(leaderId.isPresent(), "Leader found for unregistered election");
}
private class MockKubernetesLeaderElectionManager extends KubernetesLeaderElectionManager {
@Override
protected ExecutorService createExecutorService() {
return executorService;
}
@Override
protected LeaderElectionCommandProvider createLeaderElectionCommandProvider() {
return leaderElectionCommandProvider;
}
}
private static class ManagedLeaderElectionCommandProvider implements LeaderElectionCommandProvider {
private String name;
private String findLeaderName;
private boolean runStartLeading;
private boolean runStopLeading;
private boolean runNewLeader;
private boolean closed;
private String leader;
@Override
public Runnable getCommand(
final String name,
final String identity,
final Runnable onStartLeading,
final Runnable onStopLeading,
final Consumer<String> onNewLeader
) {
this.name = name;
return () -> {
if (runStartLeading) {
onStartLeading.run();
}
if (runNewLeader) {
onNewLeader.accept(identity);
}
if (runStopLeading) {
onStopLeading.run();
}
};
}
@Override
public Optional<String> findLeader(final String name) {
this.findLeaderName = name;
return Optional.ofNullable(leader);
}
@Override
public void close() {
closed = true;
}
}
}

View File

@ -0,0 +1,46 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
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.
-->
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-bundle</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-kubernetes-nar</artifactId>
<packaging>nar</packaging>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-nar</artifactId>
<version>2.0.0-SNAPSHOT</version>
<type>nar</type>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-leader-election</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-state-provider</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-kubernetes-bundle</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-kubernetes-state-provider</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-kubernetes-client</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client-api</artifactId>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-server-mock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,312 @@
/*
* 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.kubernetes.state.provider;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.StatusDetails;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.dsl.Resource;
import org.apache.nifi.components.AbstractConfigurableComponent;
import org.apache.nifi.components.state.Scope;
import org.apache.nifi.components.state.StateMap;
import org.apache.nifi.components.state.StateProvider;
import org.apache.nifi.components.state.StateProviderInitializationContext;
import org.apache.nifi.kubernetes.client.ServiceAccountNamespaceProvider;
import org.apache.nifi.kubernetes.client.StandardKubernetesClientProvider;
import org.apache.nifi.logging.ComponentLog;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* State Provider implementation based on Kubernetes ConfigMaps with Base64 encoded keys to meet Kubernetes constraints
*/
public class KubernetesConfigMapStateProvider extends AbstractConfigurableComponent implements StateProvider {
private static final Scope[] SUPPORTED_SCOPES = { Scope.CLUSTER };
private static final Charset KEY_CHARACTER_SET = StandardCharsets.UTF_8;
private static final String CONFIG_MAP_NAME_FORMAT = "nifi-component-%s";
/** Encode ConfigMap keys using URL Encoder without padding characters for compliance with Kubernetes naming */
private static final Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder decoder = Base64.getUrlDecoder();
private final AtomicBoolean enabled = new AtomicBoolean();
private KubernetesClient kubernetesClient;
private String namespace;
private String identifier;
private ComponentLog logger;
/**
* Get configured component identifier
*
* @return Component Identifier
*/
@Override
public String getIdentifier() {
return identifier;
}
/**
* Initialize Provider using configured properties
*
* @param context Initialization Context
*/
@Override
public void initialize(final StateProviderInitializationContext context) {
this.identifier = context.getIdentifier();
this.logger = context.getLogger();
this.kubernetesClient = getKubernetesClient();
this.namespace = new ServiceAccountNamespaceProvider().getNamespace();
}
/**
* Shutdown Provider
*/
@Override
public void shutdown() {
kubernetesClient.close();
logger.info("Provider shutdown");
}
/**
* Set State as ConfigMap based on Component Identifier
*
* @param state State Map
* @param componentId Component Identifier
* @throws IOException Thrown on failure to set State Map
*/
@Override
public void setState(final Map<String, String> state, final String componentId) throws IOException {
try {
final ConfigMap configMap = createConfigMapBuilder(state, componentId).build();
final ConfigMap configMapCreated = kubernetesClient.configMaps().resource(configMap).createOrReplace();
final Optional<String> version = getVersion(configMapCreated);
logger.debug("Set State Component ID [{}] Version [{}]", componentId, version);
} catch (final KubernetesClientException e) {
if (isNotFound(e.getCode())) {
logger.debug("State not found for Component ID [{}]", componentId, e);
} else {
throw new IOException(String.format("Set failed for Component ID [%s]", componentId), e);
}
} catch (final RuntimeException e) {
throw new IOException(String.format("Set failed for Component ID [%s]", componentId), e);
}
}
/**
* Get State Map for Component Identifier
*
* @param componentId Component Identifier of State to be retrieved
* @return State Map
* @throws IOException Thrown on failure to get State Map
*/
@Override
public StateMap getState(final String componentId) throws IOException {
try {
final ConfigMap configMap = configMapResource(componentId).get();
final Map<String, String> data = configMap == null ? Collections.emptyMap() : getDecodedMap(configMap.getData());
final Optional<String> version = configMap == null ? Optional.empty() : getVersion(configMap);
return new StandardStateMap(data, version);
} catch (final RuntimeException e) {
throw new IOException(String.format("Get failed for Component ID [%s]", componentId), e);
}
}
/**
* Replace State ConfigMap with new State based on current resource version
*
* @param currentState Current State Map with version
* @param state New State Map
* @param componentId Component Identifier
* @return Replace operation status
*/
@Override
public boolean replace(final StateMap currentState, final Map<String, String> state, final String componentId) throws IOException {
final ConfigMapBuilder configMapBuilder = createConfigMapBuilder(state, componentId);
final Optional<String> stateVersion = currentState.getStateVersion();
if (stateVersion.isPresent()) {
final String resourceVersion = stateVersion.get();
configMapBuilder.editOrNewMetadata().withResourceVersion(resourceVersion);
}
final ConfigMap configMap = configMapBuilder.build();
try {
final ConfigMap configMapReplaced = kubernetesClient.configMaps().resource(configMap).replace();
final Optional<String> version = getVersion(configMapReplaced);
logger.debug("Replaced State Component ID [{}] Version [{}]", componentId, version);
return true;
} catch (final KubernetesClientException e) {
if (isNotFoundOrConflict(e.getCode())) {
logger.debug("Replace State Failed Component ID [{}] Version [{}]", componentId, stateVersion, e);
return false;
} else {
throw new IOException(String.format("Replace failed for Component ID [%s]", componentId), e);
}
} catch (final RuntimeException e) {
throw new IOException(String.format("Replace failed for Component ID [%s]", componentId), e);
}
}
/**
* Clear state information for specified Component Identifier
*
* @param componentId the id of the component for which state is being cleared
* @throws IOException Thrown on failure to clear state for Component Identifier
*/
@Override
public void clear(final String componentId) throws IOException {
try {
setState(Collections.emptyMap(), componentId);
} catch (final RuntimeException e) {
throw new IOException(String.format("Clear failed for Component ID [%s]", componentId), e);
}
}
/**
* Remove state information for specified Component Identifier
*
* @param componentId Identifier of component removed from the configuration
* @throws IOException Thrown on failure to remove state for Component Identifier
*/
@Override
public void onComponentRemoved(final String componentId) throws IOException {
try {
final List<StatusDetails> deleteStatus = configMapResource(componentId).delete();
logger.debug("Config Map [{}] deleted {}", componentId, deleteStatus);
} catch (final RuntimeException e) {
throw new IOException(String.format("Remove failed for Component ID [%s]", componentId), e);
}
}
/**
* Enable Provider
*/
@Override
public void enable() {
enabled.getAndSet(true);
}
/**
* Disable Provider
*/
@Override
public void disable() {
enabled.getAndSet(false);
}
/**
* Get Enabled status
*
* @return Enabled status
*/
@Override
public boolean isEnabled() {
return enabled.get();
}
/**
* Get Supported Scopes returns CLUSTER
*
* @return Supported Scopes including CLUSTER
*/
@Override
public Scope[] getSupportedScopes() {
return SUPPORTED_SCOPES;
}
/**
* Get Kubernetes Client using standard configuration
*
* @return Kubernetes Client
*/
protected KubernetesClient getKubernetesClient() {
return new StandardKubernetesClientProvider().getKubernetesClient();
}
private Resource<ConfigMap> configMapResource(final String componentId) {
final String name = getConfigMapName(componentId);
return kubernetesClient.configMaps().inNamespace(namespace).withName(name);
}
private ConfigMapBuilder createConfigMapBuilder(final Map<String, String> state, final String componentId) {
final Map<String, String> encodedData = getEncodedMap(state);
final String name = getConfigMapName(componentId);
return new ConfigMapBuilder()
.withNewMetadata()
.withNamespace(namespace)
.withName(name)
.endMetadata()
.withData(encodedData);
}
private String getConfigMapName(final String componentId) {
return String.format(CONFIG_MAP_NAME_FORMAT, componentId);
}
private Optional<String> getVersion(final ConfigMap configMap) {
final ObjectMeta metadata = configMap.getMetadata();
final String resourceVersion = metadata.getResourceVersion();
return Optional.ofNullable(resourceVersion);
}
private Map<String, String> getEncodedMap(final Map<String, String> stateMap) {
final Map<String, String> encodedMap = new LinkedHashMap<>();
stateMap.forEach((key, value) -> {
final byte[] keyBytes = key.getBytes(KEY_CHARACTER_SET);
final String encodedKey = encoder.encodeToString(keyBytes);
encodedMap.put(encodedKey, value);
});
return encodedMap;
}
private Map<String, String> getDecodedMap(final Map<String, String> configMap) {
final Map<String, String> decodedMap = new LinkedHashMap<>();
configMap.forEach((key, value) -> {
final byte[] keyBytes = decoder.decode(key);
final String decodedKey = new String(keyBytes, KEY_CHARACTER_SET);
decodedMap.put(decodedKey, value);
});
return decodedMap;
}
private boolean isNotFound(final int code) {
return HttpURLConnection.HTTP_NOT_FOUND == code;
}
private boolean isNotFoundOrConflict(final int code) {
return isNotFound(code) || HttpURLConnection.HTTP_CONFLICT == code;
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.kubernetes.state.provider;
import org.apache.nifi.components.state.StateMap;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
* Standard implementation of StateMap
*/
class StandardStateMap implements StateMap {
private static final int EMPTY_VERSION = -1;
private final Map<String, String> data;
private final Optional<String> version;
StandardStateMap(final Map<String, String> data, final Optional<String> version) {
this.data = Collections.unmodifiableMap(data);
this.version = version;
}
/**
* Get Version returns String.hashCode() or -1 on empty for compatibility
*
* @return Version
*/
@Override
public long getVersion() {
return version.map(stateVersion -> stateVersion.hashCode()).orElse(EMPTY_VERSION);
}
/**
* Get State Version
*
* @return State Version or empty when not known
*/
@Override
public Optional<String> getStateVersion() {
return version;
}
/**
* Get Value from State Map
*
* @param key the key whose value should be retrieved
* @return Value or null when not found
*/
@Override
public String get(final String key) {
return data.get(key);
}
/**
* Get State Map
*
* @return State Map
*/
@Override
public Map<String, String> toMap() {
return data;
}
}

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.kubernetes.state.provider.KubernetesConfigMapStateProvider

View File

@ -0,0 +1,257 @@
/*
* 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.kubernetes.state.provider;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer;
import io.fabric8.kubernetes.client.server.mock.KubernetesMockServerExtension;
import io.fabric8.mockwebserver.dsl.HttpMethod;
import okhttp3.mockwebserver.RecordedRequest;
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.logging.ComponentLog;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@EnableKubernetesMockClient(crud = true)
@ExtendWith(MockitoExtension.class)
@ExtendWith(KubernetesMockServerExtension.class)
class KubernetesConfigMapStateProviderTest {
private static final String IDENTIFIER = KubernetesConfigMapStateProvider.class.getSimpleName();
private static final String FIRST_VERSION = "1";
private static final String SECOND_VERSION = "2";
private static final String DEFAULT_NAMESPACE = "default";
private static final String COMPONENT_ID = "COMPONENT-ID";
private static final String STATE_PROPERTY = "started";
private static final String STATE_PROPERTY_ENCODED = "c3RhcnRlZA";
private static final String STATE_VALUE = "now";
@Mock
StateProviderInitializationContext context;
@Mock
ComponentLog logger;
KubernetesMockServer kubernetesMockServer;
KubernetesClient kubernetesClient;
KubernetesConfigMapStateProvider provider;
@BeforeEach
void setProvider() {
provider = new MockKubernetesConfigMapStateProvider();
}
@Test
void testGetSupportedScopes() {
final Scope[] supportedScopes = provider.getSupportedScopes();
assertArrayEquals(new Scope[]{Scope.CLUSTER}, supportedScopes);
}
@Test
void testInitializeShutdown() {
setContext();
provider.initialize(context);
assertEquals(IDENTIFIER, provider.getIdentifier());
provider.shutdown();
}
@Test
void testInitializeEnableDisable() {
setContext();
provider.initialize(context);
assertEquals(IDENTIFIER, provider.getIdentifier());
assertFalse(provider.isEnabled());
provider.enable();
assertTrue(provider.isEnabled());
provider.disable();
assertFalse(provider.isEnabled());
}
@Test
void testGetStateNotFound() throws IOException {
setContext();
provider.initialize(context);
final StateMap stateMap = provider.getState(COMPONENT_ID);
assertTrue(stateMap.toMap().isEmpty());
assertFalse(stateMap.getStateVersion().isPresent());
}
@Test
void testSetStateGetState() throws IOException {
setContext();
provider.initialize(context);
final Map<String, String> state = Collections.singletonMap(STATE_PROPERTY, STATE_VALUE);
provider.setState(state, COMPONENT_ID);
final StateMap stateMap = provider.getState(COMPONENT_ID);
assertNotNull(stateMap);
final Map<String, String> stateRetrieved = stateMap.toMap();
assertEquals(state, stateRetrieved);
assertConfigMapFound();
}
@Test
void testSetStateOnComponentRemoved() throws IOException {
setContext();
provider.initialize(context);
final Map<String, String> state = Collections.singletonMap(STATE_PROPERTY, STATE_VALUE);
provider.setState(state, COMPONENT_ID);
final StateMap stateMap = provider.getState(COMPONENT_ID);
assertStateEquals(state, stateMap);
provider.onComponentRemoved(COMPONENT_ID);
final StateMap removedStateMap = provider.getState(COMPONENT_ID);
assertStateEquals(Collections.emptyMap(), removedStateMap);
}
@Test
void testClearGetState() throws IOException {
setContext();
provider.initialize(context);
provider.clear(COMPONENT_ID);
final StateMap stateMap = provider.getState(COMPONENT_ID);
assertNotNull(stateMap);
final Map<String, String> stateRetrieved = stateMap.toMap();
assertTrue(stateRetrieved.isEmpty());
}
@Test
void testReplaceNotFound() throws IOException {
setContext();
provider.initialize(context);
final StateMap stateMap = new StandardStateMap(Collections.emptyMap(), Optional.empty());
final boolean replaced = provider.replace(stateMap, Collections.emptyMap(), COMPONENT_ID);
assertFalse(replaced);
}
@Test
void testSetStateReplace() throws IOException {
setContext();
provider.initialize(context);
final Map<String, String> state = Collections.singletonMap(STATE_PROPERTY, STATE_VALUE);
provider.setState(state, COMPONENT_ID);
final StateMap initialStateMap = provider.getState(COMPONENT_ID);
final Optional<String> initialVersion = initialStateMap.getStateVersion();
assertTrue(initialVersion.isPresent());
assertEquals(FIRST_VERSION, initialVersion.get());
final boolean replaced = provider.replace(initialStateMap, Collections.emptyMap(), COMPONENT_ID);
assertTrue(replaced);
final StateMap replacedStateMap = provider.getState(COMPONENT_ID);
final Optional<String> replacedVersion = replacedStateMap.getStateVersion();
assertTrue(replacedVersion.isPresent());
assertEquals(SECOND_VERSION, replacedVersion.get());
}
@Test
void testOnComponentRemovedNotFound() throws IOException, InterruptedException {
setContext();
provider.initialize(context);
provider.onComponentRemoved(COMPONENT_ID);
final RecordedRequest request = kubernetesMockServer.getLastRequest();
assertEquals(HttpMethod.DELETE.name(), request.getMethod());
}
private void setContext() {
when(context.getIdentifier()).thenReturn(IDENTIFIER);
when(context.getLogger()).thenReturn(logger);
}
private void assertStateEquals(final Map<String, String> expected, final StateMap stateMap) {
assertNotNull(stateMap);
final Map<String, String> stateRetrieved = stateMap.toMap();
assertEquals(expected, stateRetrieved);
}
private void assertConfigMapFound() {
final ConfigMapList configMapList = kubernetesClient.configMaps().inNamespace(DEFAULT_NAMESPACE).list();
final Optional<ConfigMap> configMapFound = configMapList.getItems()
.stream()
.filter(configMap -> configMap.getMetadata().getName().endsWith(COMPONENT_ID))
.findFirst();
assertTrue(configMapFound.isPresent());
final ConfigMap configMap = configMapFound.get();
final Map<String, String> configMapData = configMap.getData();
final Map<String, String> expectedData = Collections.singletonMap(STATE_PROPERTY_ENCODED, STATE_VALUE);
assertEquals(expectedData, configMapData);
}
private class MockKubernetesConfigMapStateProvider extends KubernetesConfigMapStateProvider {
@Override
protected KubernetesClient getKubernetesClient() {
return kubernetesClient;
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-kubernetes-bundle</artifactId>
<packaging>pom</packaging>
<modules>
<module>nifi-framework-kubernetes-leader-election</module>
<module>nifi-framework-kubernetes-state-provider</module>
<module>nifi-framework-kubernetes-nar</module>
</modules>
</project>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-leader-election-shared</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-utils</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,43 @@
/*
* 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.leader.election;
/**
* Leader Election Role enumeration for mapping public role to RFC 1123 subdomain-style names
*/
public enum LeaderElectionRole {
CLUSTER_COORDINATOR("Cluster Coordinator", "cluster-coordinator"),
PRIMARY_NODE("Primary Node", "primary-node");
private final String roleName;
private final String roleId;
LeaderElectionRole(final String roleName, final String roleId) {
this.roleName = roleName;
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public String getRoleId() {
return roleId;
}
}

View File

@ -0,0 +1,147 @@
/*
* 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.leader.election;
import org.apache.nifi.util.timebuffer.CountSumMinMaxAccess;
import org.apache.nifi.util.timebuffer.LongEntityAccess;
import org.apache.nifi.util.timebuffer.TimedBuffer;
import org.apache.nifi.util.timebuffer.TimestampedLong;
import org.apache.nifi.util.timebuffer.TimestampedLongAggregation;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Abstract implementation of Leader Election Manager supporting tracking of operations
*/
public abstract class TrackedLeaderElectionManager implements LeaderElectionManager {
private final ConcurrentMap<String, TimedBuffer<TimestampedLong>> leaderChanges = new ConcurrentHashMap<>();
private final TimedBuffer<TimestampedLongAggregation> pollTimes = new TimedBuffer<>(TimeUnit.SECONDS, 300, new CountSumMinMaxAccess());
/**
* Register as observer without Participant Identifier
*
* @param roleName Name of role to be registered for elections
* @param listener Listener notified on leader state changes
*/
@Override
public void register(final String roleName, final LeaderElectionStateChangeListener listener) {
register(roleName, listener, null);
}
@Override
public Map<String, Integer> getLeadershipChangeCount(final long duration, final TimeUnit unit) {
final Map<String, Integer> leadershipChangesPerRole = new LinkedHashMap<>();
for (final Map.Entry<String, TimedBuffer<TimestampedLong>> entry : leaderChanges.entrySet()) {
final String roleName = entry.getKey();
final TimedBuffer<TimestampedLong> buffer = entry.getValue();
final TimestampedLong aggregateValue = buffer.getAggregateValue(System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(duration, unit));
final int leadershipChanges = aggregateValue.getValue().intValue();
leadershipChangesPerRole.put(roleName, leadershipChanges);
}
return leadershipChangesPerRole;
}
@Override
public long getAveragePollTime(final TimeUnit timeUnit) {
final long averageNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null || aggregation.getCount() == 0) {
return 0L;
}
averageNanos = aggregation.getSum() / aggregation.getCount();
}
return timeUnit.convert(averageNanos, TimeUnit.NANOSECONDS);
}
@Override
public long getMinPollTime(final TimeUnit timeUnit) {
final long minNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
minNanos = aggregation.getMin();
}
return timeUnit.convert(minNanos, TimeUnit.NANOSECONDS);
}
@Override
public long getMaxPollTime(final TimeUnit timeUnit) {
final long maxNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
maxNanos = aggregation.getMax();
}
return timeUnit.convert(maxNanos, TimeUnit.NANOSECONDS);
}
@Override
public long getPollCount() {
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
return aggregation.getCount();
}
}
/**
* Register Poll Time in nanoseconds
*
* @param nanos Elapsed System Time in nanoseconds
*/
protected void registerPollTime(final long nanos) {
synchronized (pollTimes) {
pollTimes.add(TimestampedLongAggregation.newValue(nanos));
}
}
/**
* On Leader Changed register role name changes
*
* @param roleName Role Name for leader changes
*/
protected void onLeaderChanged(final String roleName) {
final TimedBuffer<TimestampedLong> buffer = leaderChanges.computeIfAbsent(roleName, key -> new TimedBuffer<>(TimeUnit.HOURS, 24, new LongEntityAccess()));
buffer.add(new TimestampedLong(1L));
}
/**
* Is specified identifier participating in the election based on null or empty participant identifier
*
* @param participantId Participant Identifier
* @return Participating status
*/
protected boolean isParticipating(final String participantId) {
return participantId != null && !participantId.trim().isEmpty();
}
}

View File

@ -28,6 +28,7 @@ import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.state.StateProvider;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.controller.leader.election.LeaderElectionManager;
import org.apache.nifi.controller.repository.ContentRepository;
import org.apache.nifi.controller.repository.FlowFileRepository;
import org.apache.nifi.controller.repository.FlowFileSwapManager;
@ -117,6 +118,7 @@ public class StandardExtensionDiscoveringManager implements ExtensionDiscovering
definitionMap.put(NarProvider.class, new HashSet<>());
definitionMap.put(ExternalResourceProvider.class, new HashSet<>());
definitionMap.put(FlowRegistryClient.class, new HashSet<>());
definitionMap.put(LeaderElectionManager.class, new HashSet<>());
additionalExtensionTypes.forEach(type -> definitionMap.putIfAbsent(type, new HashSet<>()));
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework</artifactId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>nifi-framework-zookeeper-leader-election</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-leader-election-shared</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-cluster-zookeeper</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-properties</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-core-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
</dependency>
<!-- metrics-core required for ZooKeeper Server -->
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
</dependency>
<!-- snappy-java required for ZooKeeper Server -->
<dependency>
<groupId>org.xerial.snappy</groupId>
<artifactId>snappy-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.nifi.controller.leader.election;
package org.apache.nifi.framework.cluster.leader.zookeeper;
import java.util.ArrayList;
import java.util.Arrays;
@ -24,7 +24,7 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.api.ACLProvider;
import org.apache.curator.framework.imps.DefaultACLProvider;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Id;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.controller.leader.election;
package org.apache.nifi.framework.cluster.leader.zookeeper;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.RetryPolicy;
@ -27,14 +27,11 @@ import org.apache.curator.framework.recipes.leader.Participant;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.retry.RetryNTimes;
import org.apache.curator.utils.ZookeeperFactory;
import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
import org.apache.nifi.controller.leader.election.TrackedLeaderElectionManager;
import org.apache.nifi.engine.FlowEngine;
import org.apache.nifi.framework.cluster.zookeeper.ZooKeeperClientConfig;
import org.apache.nifi.controller.leader.election.LeaderElectionStateChangeListener;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.timebuffer.CountSumMinMaxAccess;
import org.apache.nifi.util.timebuffer.LongEntityAccess;
import org.apache.nifi.util.timebuffer.TimedBuffer;
import org.apache.nifi.util.timebuffer.TimestampedLong;
import org.apache.nifi.util.timebuffer.TimestampedLongAggregation;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
@ -47,17 +44,20 @@ import org.apache.zookeeper.common.ZKConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class CuratorLeaderElectionManager implements LeaderElectionManager {
public class CuratorLeaderElectionManager extends TrackedLeaderElectionManager {
private static final String OBSERVER_ID = "OBSERVER";
private static final Logger logger = LoggerFactory.getLogger(CuratorLeaderElectionManager.class);
private final FlowEngine leaderElectionMonitorEngine;
private final ExecutorService leaderElectionMonitorEngine = new FlowEngine(4, CuratorLeaderElectionManager.class.getSimpleName());
private final ZooKeeperClientConfig zkConfig;
private CuratorFramework curatorClient;
@ -65,14 +65,17 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
private volatile boolean stopped = true;
private final ConcurrentMap<String, LeaderRole> leaderRoles = new ConcurrentHashMap<>();
private final ConcurrentMap<String, RegisteredRole> registeredRoles = new ConcurrentHashMap<>();
private final ConcurrentMap<String, TimedBuffer<TimestampedLong>> leaderChanges = new ConcurrentHashMap<>();
private final TimedBuffer<TimestampedLongAggregation> pollTimes = new TimedBuffer<>(TimeUnit.SECONDS, 300, new CountSumMinMaxAccess());
private final ConcurrentMap<String, String> lastKnownLeader = new ConcurrentHashMap<>();
public CuratorLeaderElectionManager(final int threadPoolSize, final NiFiProperties properties) {
leaderElectionMonitorEngine = new FlowEngine(threadPoolSize, "Leader Election Notification", true);
/**
* Curator Leader Election Manager constructor with NiFi Properties for NarThreadContextClassLoader.createInstance()
*
* @param properties NiFi Properties
*/
public CuratorLeaderElectionManager(final NiFiProperties properties) {
zkConfig = ZooKeeperClientConfig.createConfig(properties);
}
@ -108,11 +111,6 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
return participantId != null;
}
@Override
public void register(String roleName, LeaderElectionStateChangeListener listener) {
register(roleName, listener, null);
}
private String getElectionPath(final String roleName) {
final String rootPath = zkConfig.getRootPath();
final String leaderPath = rootPath + (rootPath.endsWith("/") ? "" : "/") + "leaders/" + roleName;
@ -141,27 +139,24 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
registeredRoles.put(roleName, new RegisteredRole(participantId, listener));
final boolean isParticipant = participantId != null && !participantId.trim().isEmpty();
final boolean participating = isParticipating(participantId);
if (!isStopped()) {
final ElectionListener electionListener = new ElectionListener(roleName, listener, participantId);
final LeaderSelector leaderSelector = new LeaderSelector(curatorClient, leaderPath, leaderElectionMonitorEngine, electionListener);
if (isParticipant) {
if (participating) {
leaderSelector.autoRequeue();
leaderSelector.setId(participantId);
leaderSelector.start();
}
final LeaderRole leaderRole = new LeaderRole(leaderSelector, electionListener, isParticipant);
final LeaderRole leaderRole = new LeaderRole(leaderSelector, electionListener, participating);
leaderRoles.put(roleName, leaderRole);
}
if (isParticipant) {
logger.info("{} Registered new Leader Selector for role {}; this node is an active participant in the election.", this, roleName);
} else {
logger.info("{} Registered new Leader Selector for role {}; this node is a silent observer in the election.", this, roleName);
}
final String registeredId = participating ? participantId : OBSERVER_ID;
logger.info("Registered for Election: Role [{}] Registered ID [{}]", roleName, registeredId);
}
@Override
@ -183,7 +178,7 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
leaderRole.getElectionListener().disable();
leaderSelector.close();
logger.info("This node is no longer registered to be elected as the Leader for Role '{}'", roleName);
logger.info("Unregistered for Election: Role [{}]", roleName);
}
@Override
@ -208,11 +203,11 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
curatorClient = null;
}
leaderElectionMonitorEngine.shutdown();
logger.info("{} stopped and closed", this);
}
@Override
public boolean isStopped() {
private boolean isStopped() {
return stopped;
}
@ -225,26 +220,6 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
return leaderRoles.get(roleName);
}
private void onLeaderChanged(final String roleName) {
final TimedBuffer<TimestampedLong> buffer = leaderChanges.computeIfAbsent(roleName, key -> new TimedBuffer<>(TimeUnit.HOURS, 24, new LongEntityAccess()));
buffer.add(new TimestampedLong(1L));
}
public Map<String, Integer> getLeadershipChangeCount(final long duration, final TimeUnit unit) {
final Map<String, Integer> leadershipChangesPerRole = new HashMap<>();
for (final Map.Entry<String, TimedBuffer<TimestampedLong>> entry : leaderChanges.entrySet()) {
final String roleName = entry.getKey();
final TimedBuffer<TimestampedLong> buffer = entry.getValue();
final TimestampedLong aggregateValue = buffer.getAggregateValue(System.currentTimeMillis() - TimeUnit.MILLISECONDS.convert(duration, unit));
final int leadershipChanges = aggregateValue.getValue().intValue();
leadershipChangesPerRole.put(roleName, leadershipChanges);
}
return leadershipChangesPerRole;
}
@Override
public boolean isLeader(final String roleName) {
final boolean activeParticipant = isActiveParticipant(roleName);
@ -263,7 +238,7 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
}
@Override
public String getLeader(final String roleName) {
public Optional<String> getLeader(final String roleName) {
if (isStopped()) {
return determineLeaderExternal(roleName);
}
@ -280,19 +255,19 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
try {
participant = role.getLeaderSelector().getLeader();
} catch (Exception e) {
logger.warn("Unable to determine leader for role '{}'; returning null", roleName, e);
return null;
logger.warn("Unable to determine leader for role [{}]", roleName, e);
return Optional.empty();
}
if (participant == null) {
logger.debug("There is currently no elected leader for the {} role", roleName);
return null;
return Optional.empty();
}
final String participantId = participant.getId();
if (StringUtils.isEmpty(participantId)) {
logger.debug("Found leader participant for role {} but the participantId was empty", roleName);
return null;
logger.debug("Found leader participant for role [{}] but the participantId was empty", roleName);
return Optional.empty();
}
final String previousLeader = lastKnownLeader.put(roleName, participantId);
@ -300,86 +275,20 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
onLeaderChanged(roleName);
}
return participantId;
return Optional.of(participantId);
} finally {
registerPollTime(System.nanoTime() - startNanos);
}
}
private void registerPollTime(final long nanos) {
synchronized (pollTimes) {
pollTimes.add(TimestampedLongAggregation.newValue(nanos));
}
}
public long getAveragePollTime(final TimeUnit timeUnit) {
final long averageNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null || aggregation.getCount() == 0) {
return 0L;
}
averageNanos = aggregation.getSum() / aggregation.getCount();
}
return timeUnit.convert(averageNanos, TimeUnit.NANOSECONDS);
}
public long getMinPollTime(final TimeUnit timeUnit) {
final long minNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
minNanos = aggregation.getMin();
}
return timeUnit.convert(minNanos, TimeUnit.NANOSECONDS);
}
public long getMaxPollTime(final TimeUnit timeUnit) {
final long maxNanos;
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
maxNanos = aggregation.getMax();
}
return timeUnit.convert(maxNanos, TimeUnit.NANOSECONDS);
}
@Override
public long getPollCount() {
synchronized (pollTimes) {
final TimestampedLongAggregation.TimestampedAggregation aggregation = pollTimes.getAggregateValue(0L).getAggregation();
if (aggregation == null) {
return 0L;
}
return aggregation.getCount();
}
}
/**
* Determines whether or not leader election has already begun for the role with the given name
*
* @param roleName the role of interest
* @return <code>true</code> if leader election has already begun, <code>false</code> if it has not or if unable to determine this.
*/
@Override
public boolean isLeaderElected(final String roleName) {
final String leaderAddress = determineLeaderExternal(roleName);
return !StringUtils.isEmpty(leaderAddress);
}
/**
* Use a new Curator client to determine which node is the elected leader for the given role.
*
* @param roleName the name of the role
* @return the id of the elected leader, or <code>null</code> if no leader has been selected or if unable to determine
* @return the id of the elected leader, or <code>Optional.empty()</code> if no leader has been selected or if unable to determine
* the leader from ZooKeeper
*/
private String determineLeaderExternal(final String roleName) {
private Optional<String> determineLeaderExternal(final String roleName) {
final long start = System.nanoTime();
try (CuratorFramework client = createClient()) {
@ -401,17 +310,17 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
try {
final Participant leader = selector.getLeader();
return leader == null ? null : leader.getId();
return leader == null ? Optional.empty() : Optional.of(leader.getId());
} catch (final KeeperException.NoNodeException nne) {
// If there is no ZNode, then there is no elected leader.
return null;
return Optional.empty();
} catch (final Exception e) {
logger.warn("Unable to determine the Elected Leader for role '{}' due to {}; assuming no leader has been elected", roleName, e.toString());
if (logger.isDebugEnabled()) {
logger.warn("", e);
}
return null;
return Optional.empty();
}
} finally {
registerPollTime(System.nanoTime() - start);
@ -569,13 +478,14 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
* @return <code>true</code> if this node is still the elected leader according to ZooKeeper, false otherwise
*/
private boolean verifyLeader() {
final String leader = getLeader(roleName);
if (leader == null) {
final Optional<String> leaderAddress = getLeader(roleName);
if (!leaderAddress.isPresent()) {
logger.debug("Reached out to ZooKeeper to determine which node is the elected leader for Role '{}' but found that there is no leader.", roleName);
setLeader(false);
return false;
}
final String leader = leaderAddress.get();
final boolean match = leader.equals(participantId);
logger.debug("Reached out to ZooKeeper to determine which node is the elected leader for Role '{}'. Elected Leader = '{}', Participant ID = '{}', This Node Elected = {}",
roleName, leader, participantId, match);
@ -591,7 +501,7 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
if (listener != null) {
try {
listener.onLeaderElection();
listener.onStartLeading();
} catch (final Exception e) {
logger.error("This node was elected Leader for Role '{}' but failed to take leadership. Will relinquish leadership role. Failure was due to: {}", roleName, e);
setLeader(false);
@ -644,7 +554,7 @@ public class CuratorLeaderElectionManager implements LeaderElectionManager {
if (listener != null) {
try {
listener.onLeaderRelinquish();
listener.onStopLeading();
} catch (final Exception e) {
logger.error("This node is no longer leader for role '{}' but failed to shutdown leadership responsibilities properly due to: {}", roleName, e.toString());
if (logger.isDebugEnabled()) {

View File

@ -0,0 +1,15 @@
# 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.
org.apache.nifi.framework.cluster.leader.zookeeper.CuratorLeaderElectionManager

View File

@ -201,6 +201,7 @@
<!-- nifi.properties: cluster node properties (only configure for cluster nodes) -->
<nifi.cluster.is.node>false</nifi.cluster.is.node>
<nifi.cluster.leader.election.implementation>CuratorLeaderElectionManager</nifi.cluster.leader.election.implementation>
<nifi.cluster.node.address />
<nifi.cluster.node.protocol.port />
<nifi.cluster.node.protocol.max.threads>50</nifi.cluster.node.protocol.max.threads>

View File

@ -257,6 +257,7 @@ nifi.cluster.protocol.is.secure=${nifi.cluster.protocol.is.secure}
# cluster node properties (only configure for cluster nodes) #
nifi.cluster.is.node=${nifi.cluster.is.node}
nifi.cluster.leader.election.implementation=${nifi.cluster.leader.election.implementation}
nifi.cluster.node.address=${nifi.cluster.node.address}
nifi.cluster.node.protocol.port=${nifi.cluster.node.protocol.port}
nifi.cluster.node.protocol.max.threads=${nifi.cluster.node.protocol.max.threads}

View File

@ -64,6 +64,12 @@
<property name="Access Control">Open</property>
</cluster-provider>
<!-- Kubernetes ConfigMap implementation of State Provider -->
<cluster-provider>
<id>kubernetes-provider</id>
<class>org.apache.nifi.kubernetes.state.provider.KubernetesConfigMapStateProvider</class>
</cluster-provider>
<!--
Cluster State Provider that stores state in Redis. This can be used as an alternative to the ZooKeeper State Provider.

View File

@ -6289,12 +6289,13 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
final String nodeAddress = nodeId.getSocketAddress() + ":" + nodeId.getSocketPort();
for (final String roleName : ClusterRoles.getAllRoles()) {
final String leader = leaderElectionManager.getLeader(roleName);
if (leader == null) {
final Optional<String> leader = leaderElectionManager.getLeader(roleName);
if (!leader.isPresent()) {
continue;
}
if (leader.equals(nodeAddress)) {
final String leaderAddress = leader.get();
if (leaderAddress.equals(nodeAddress)) {
roles.add(roleName);
}
}

View File

@ -33,7 +33,11 @@
<module>nifi-framework-components</module>
<module>nifi-framework-core</module>
<module>nifi-framework-cluster-protocol</module>
<module>nifi-framework-cluster-zookeeper</module>
<module>nifi-framework-cluster</module>
<module>nifi-framework-leader-election-shared</module>
<module>nifi-framework-zookeeper-leader-election</module>
<module>nifi-framework-kubernetes-bundle</module>
<module>nifi-framework-nar-utils</module>
<module>nifi-framework-nar-loading-utils</module>
<module>nifi-user-actions</module>

View File

@ -318,7 +318,7 @@ public class ListGCSBucket extends AbstractGCSProcessor {
void restoreState(final ProcessSession session) throws IOException {
final StateMap stateMap = session.getState(Scope.CLUSTER);
if (stateMap.getVersion() == -1L || stateMap.get(CURRENT_TIMESTAMP) == null || stateMap.get(CURRENT_KEY_PREFIX+"0") == null) {
if (!stateMap.getStateVersion().isPresent() || stateMap.get(CURRENT_TIMESTAMP) == null || stateMap.get(CURRENT_KEY_PREFIX+"0") == null) {
currentTimestamp = 0L;
currentKeys.clear();
} else {

View File

@ -141,7 +141,7 @@ public class ListGCSBucketTest extends AbstractGCSTest {
addRequiredPropertiesToRunner(runner);
runner.assertValid();
assertEquals(-1L, runner.getProcessContext().getStateManager().getState(Scope.CLUSTER).getVersion(), "Cluster StateMap should be fresh (version -1L)");
assertFalse(runner.getProcessContext().getStateManager().getState(Scope.CLUSTER).getStateVersion().isPresent(), "Cluster StateMap should be fresh (version -1L)");
assertTrue(processor.getStateKeys().isEmpty());
processor.restoreState(runner.getProcessSessionFactory().createSession());
@ -187,9 +187,9 @@ public class ListGCSBucketTest extends AbstractGCSTest {
addRequiredPropertiesToRunner(runner);
runner.assertValid();
assertEquals(-1L,
runner.getProcessContext().getStateManager().getState(Scope.CLUSTER).getVersion(),
"Cluster StateMap should be fresh (version -1L)"
assertFalse(
runner.getProcessContext().getStateManager().getState(Scope.CLUSTER).getStateVersion().isPresent(),
"Cluster StateMap should be fresh"
);
final Set<String> keys = new LinkedHashSet<>(Arrays.asList("test-key-0", "test-key-1"));
@ -197,7 +197,7 @@ public class ListGCSBucketTest extends AbstractGCSTest {
processor.persistState(session, 4L, keys);
final StateMap stateMap = runner.getStateManager().getState(Scope.CLUSTER);
assertEquals(1L, stateMap.getVersion(), "Cluster StateMap should have been written to");
assertTrue(stateMap.getStateVersion().isPresent(), "Cluster StateMap should have been written to");
final Map<String, String> state = new HashMap<>();
state.put(ListGCSBucket.CURRENT_TIMESTAMP, String.valueOf(4L));
@ -369,7 +369,7 @@ public class ListGCSBucketTest extends AbstractGCSTest {
runner.assertTransferCount(ListGCSBucket.REL_SUCCESS, 0);
verifyConfigVerification(runner, processor, 0);
assertEquals(-1L, runner.getStateManager().getState(Scope.CLUSTER).getVersion(), "No state should be persisted on an empty return");
assertFalse(runner.getStateManager().getState(Scope.CLUSTER).getStateVersion().isPresent(), "No state should be persisted on an empty return");
}
@Test

View File

@ -420,7 +420,7 @@ public class ListHDFS extends AbstractHadoopProcessor {
// Ensure that we are using the latest listing information before we try to perform a listing of HDFS files.
try {
final StateMap stateMap = session.getState(Scope.CLUSTER);
if (stateMap.getVersion() == -1L) {
if (!stateMap.getStateVersion().isPresent()) {
latestTimestampEmitted = -1L;
latestTimestampListed = -1L;
getLogger().debug("Found no state stored");

View File

@ -207,7 +207,7 @@ public class GetHBase extends AbstractProcessor implements VisibilityFetchSuppor
@OnScheduled
public void parseColumns(final ProcessContext context) throws IOException {
final StateMap stateMap = context.getStateManager().getState(Scope.CLUSTER);
if (stateMap.getVersion() < 0) {
if (!stateMap.getStateVersion().isPresent()) {
// no state has been stored in the State Manager - check if we have state stored in the
// DistributedMapCacheClient service and migrate it if so
final DistributedMapCacheClient client = context.getProperty(DISTRIBUTED_CACHE_SERVICE).asControllerService(DistributedMapCacheClient.class);
@ -461,7 +461,7 @@ public class GetHBase extends AbstractProcessor implements VisibilityFetchSuppor
private ScanResult getState(final ProcessSession session) throws IOException {
final StateMap stateMap = session.getState(Scope.CLUSTER);
if (stateMap.getVersion() < 0) {
if (!stateMap.getStateVersion().isPresent()) {
return null;
}

View File

@ -21,6 +21,7 @@ import org.apache.nifi.components.state.StateMap;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
/**
@ -49,6 +50,17 @@ public class RedisStateMap implements StateMap {
return version;
}
@Override
public Optional<String> getStateVersion() {
final String stateVersion;
if (DEFAULT_VERSION == version || version == null) {
stateVersion = null;
} else {
stateVersion = String.valueOf(version);
}
return Optional.ofNullable(stateVersion);
}
@Override
public String get(String key) {
return stateValues.get(key);

View File

@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
/**
* A RedisStateMapSerDe that uses JSON as the underlying representation.
@ -35,6 +36,8 @@ public class RedisStateMapJsonSerDe implements RedisStateMapSerDe {
public static final String FIELD_ENCODING = "encodingVersion";
public static final String FIELD_STATE_VALUES = "stateValues";
static final long EMPTY_VERSION = -1;
private final JsonFactory jsonFactory = new JsonFactory(new ObjectMapper());
@Override
@ -46,7 +49,10 @@ public class RedisStateMapJsonSerDe implements RedisStateMapSerDe {
try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
final JsonGenerator jsonGenerator = jsonFactory.createGenerator(out);
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField(FIELD_VERSION, stateMap.getVersion());
final Optional<String> stateVersion = stateMap.getStateVersion();
final long version = stateVersion.map(Long::parseLong).orElse(EMPTY_VERSION);
jsonGenerator.writeNumberField(FIELD_VERSION, version);
jsonGenerator.writeNumberField(FIELD_ENCODING, stateMap.getEncodingVersion());
jsonGenerator.writeObjectFieldStart(FIELD_STATE_VALUES);

View File

@ -41,6 +41,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* A StateProvider backed by Redis.
@ -212,14 +213,14 @@ public class RedisStateProvider extends AbstractConfigurableComponent implements
final byte[] key = getComponentKey(componentId).getBytes(StandardCharsets.UTF_8);
redisConnection.watch(key);
final long prevVersion = oldValue == null ? -1L : oldValue.getVersion();
final Optional<String> previousVersion = oldValue == null ? Optional.empty() : oldValue.getStateVersion();
final byte[] currValue = redisConnection.get(key);
final RedisStateMap currStateMap = serDe.deserialize(currValue);
final long currVersion = currStateMap == null ? -1L : currStateMap.getVersion();
final Optional<String> currentVersion = currStateMap == null ? Optional.empty() : currStateMap.getStateVersion();
// the replace API expects that you can't call replace on a non-existing value, so unwatch and return
if (!allowReplaceMissing && currVersion == -1) {
if (!allowReplaceMissing && !currentVersion.isPresent()) {
redisConnection.unwatch();
return false;
}
@ -228,10 +229,11 @@ public class RedisStateProvider extends AbstractConfigurableComponent implements
redisConnection.multi();
// compare-and-set
if (prevVersion == currVersion) {
if (previousVersion.equals(currentVersion)) {
// build the new RedisStateMap incrementing the version, using latest encoding, and using the passed in values
final long currentVersionNumber = currentVersion.map(Long::parseLong).orElse(RedisStateMapJsonSerDe.EMPTY_VERSION);
final RedisStateMap newStateMap = new RedisStateMap.Builder()
.version(currVersion + 1)
.version(currentVersionNumber + 1)
.encodingVersion(ENCODING_VERSION)
.stateValues(newValue)
.build();
@ -263,7 +265,7 @@ public class RedisStateProvider extends AbstractConfigurableComponent implements
updated = replace(currStateMap, Collections.emptyMap(), componentId, true);
final String result = updated ? "successful" : "unsuccessful";
logger.debug("Attempt # {} to clear state for component {} was {}", new Object[] { attempted + 1, componentId, result});
logger.debug("Attempt # {} to clear state for component {} was {}", attempted + 1, componentId, result);
attempted++;
}

View File

@ -101,7 +101,7 @@ public class ITRedisStateProvider {
StateMap map = provider.getState(componentId);
assertNotNull(map);
assertEquals(-1, map.getVersion());
assertFalse(map.getStateVersion().isPresent());
assertNotNull(map.toMap());
assertTrue(map.toMap().isEmpty());
@ -109,7 +109,7 @@ public class ITRedisStateProvider {
map = provider.getState(componentId);
assertNotNull(map);
assertEquals(0, map.getVersion());
assertTrue(map.getStateVersion().isPresent());
assertEquals("value1", map.get(key));
assertEquals("value1", map.toMap().get(key));
@ -119,7 +119,7 @@ public class ITRedisStateProvider {
map = provider.getState(componentId);
assertEquals("value2", map.get(key));
assertEquals(1L, map.getVersion());
assertTrue(map.getStateVersion().isPresent());
}
@Test
@ -131,7 +131,7 @@ public class ITRedisStateProvider {
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals("value1", stateMap.get(key));
assertEquals(0, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
provider.setState(Collections.singletonMap(key, "intermediate value"), componentId);
@ -140,7 +140,7 @@ public class ITRedisStateProvider {
assertEquals(key, stateMap.toMap().keySet().iterator().next());
assertEquals(1, stateMap.toMap().size());
assertEquals("intermediate value", stateMap.get(key));
assertEquals(1, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
}
@ -164,7 +164,7 @@ public class ITRedisStateProvider {
map = stateMap.toMap();
assertNotNull(map);
assertTrue(map.isEmpty());
assertEquals(1, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
}
@Test
@ -172,21 +172,21 @@ public class ITRedisStateProvider {
final StateProvider provider = getProvider();
StateMap stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(-1L, stateMap.getVersion());
assertFalse(stateMap.getStateVersion().isPresent());
assertTrue(stateMap.toMap().isEmpty());
provider.setState(Collections.singletonMap("testClear", "value"), componentId);
stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(0, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
assertEquals("value", stateMap.get("testClear"));
provider.clear(componentId);
stateMap = provider.getState(componentId);
assertNotNull(stateMap);
assertEquals(1L, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
assertTrue(stateMap.toMap().isEmpty());
}
@ -238,7 +238,7 @@ public class ITRedisStateProvider {
provider.setState(newValue, componentId);
final StateMap stateMap = provider.getState(componentId);
assertEquals(0L, stateMap.getVersion());
assertTrue(stateMap.getStateVersion().isPresent());
provider.onComponentRemoved(componentId);
@ -247,8 +247,8 @@ public class ITRedisStateProvider {
final StateMap stateMapAfterRemoval = provider.getState(componentId);
// version should be -1 because the state has been removed entirely.
assertEquals(-1L, stateMapAfterRemoval.getVersion());
// version should be not present because the state has been removed entirely.
assertFalse(stateMap.getStateVersion().isPresent());
}

View File

@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -49,7 +50,7 @@ public class TestRedisStateMapJsonSerDe {
final RedisStateMap deserialized = serDe.deserialize(serialized);
assertNotNull(deserialized);
assertEquals(stateMap.getVersion(), deserialized.getVersion());
assertEquals(stateMap.getStateVersion(), deserialized.getStateVersion());
assertEquals(stateMap.getEncodingVersion(), deserialized.getEncodingVersion());
assertEquals(stateMap.toMap(), deserialized.toMap());
}
@ -73,7 +74,7 @@ public class TestRedisStateMapJsonSerDe {
final RedisStateMap deserialized = serDe.deserialize(serialized);
assertNotNull(deserialized);
assertEquals(RedisStateMap.DEFAULT_VERSION.longValue(), stateMap.getVersion());
assertFalse(stateMap.getStateVersion().isPresent());
assertEquals(RedisStateMap.DEFAULT_ENCODING, stateMap.getEncodingVersion());
assertNotNull(deserialized.toMap());
assertEquals(0, deserialized.toMap().size());

View File

@ -590,7 +590,7 @@ public class GetSplunk extends AbstractProcessor {
private TimeRange loadState(final ProcessSession session) throws IOException {
final StateMap stateMap = session.getState(Scope.CLUSTER);
if (stateMap.getVersion() < 0) {
if (!stateMap.getStateVersion().isPresent()) {
getLogger().debug("No previous state found");
return null;
}

View File

@ -329,7 +329,7 @@ public class TestGetSplunk {
final StateMap state = runner.getStateManager().getState(Scope.CLUSTER);
assertNotNull(state);
assertTrue(state.getVersion() > 0);
assertTrue(state.getStateVersion().isPresent());
// save the latest time from the first run which should be earliest time of next run
final String lastLatest = state.get(GetSplunk.LATEST_TIME_KEY);
@ -377,7 +377,7 @@ public class TestGetSplunk {
final StateMap state = runner.getStateManager().getState(Scope.CLUSTER);
assertNotNull(state);
assertTrue(state.getVersion() > 0);
assertTrue(state.getStateVersion().isPresent());
// save the latest time from the first run which should be earliest time of next run
final String lastLatest = state.get(GetSplunk.LATEST_TIME_KEY);

View File

@ -346,7 +346,7 @@ public class MonitorActivity extends AbstractProcessor {
}
newValues.put(STATE_KEY_LATEST_SUCCESS_TRANSFER, String.valueOf(now));
if (state == null || state.getVersion() == -1) {
if (state == null || !state.getStateVersion().isPresent()) {
session.setState(newValues, Scope.CLUSTER);
} else {
final String existingTimestamp = state.get(STATE_KEY_LATEST_SUCCESS_TRANSFER);

View File

@ -414,7 +414,7 @@ public class TailFile extends AbstractProcessor {
final String startPosition = context.getProperty(START_POSITION).getValue();
if (stateMap.getVersion() == -1L || stateMap.toMap().isEmpty()) {
if (!stateMap.getStateVersion().isPresent() || stateMap.toMap().isEmpty()) {
//state has been cleared or never stored so recover as 'empty state'
initStates(filesToTail, Collections.emptyMap(), true, startPosition);
recoverState(context, filesToTail, Collections.emptyMap());

View File

@ -28,9 +28,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class HashMapStateProvider implements StateProvider {
private static final int UNKNOWN_STATE_VERSION = -1;
private static final long VERSION_INCREMENT = 1;
private static final String INITIAL_VERSION = String.valueOf(VERSION_INCREMENT);
private final Map<String, StateMap> committedStates = new HashMap<>();
private final Map<String, StateMap> activeStates = new HashMap<>();
@ -67,8 +70,9 @@ public class HashMapStateProvider implements StateProvider {
@Override
public synchronized void setState(final Map<String, String> state, final String componentId) {
final StateMap existing = getState(componentId);
final long version = existing == null ? UNKNOWN_STATE_VERSION : existing.getVersion();
final StateMap updated = new StandardStateMap(state, version + 1);
final Optional<String> existingVersion = existing.getStateVersion();
final String version = existingVersion.map(this::getIncrementedVersion).orElse(INITIAL_VERSION);
final StateMap updated = new StandardStateMap(state, Optional.of(version));
activeStates.put(componentId, updated);
}
@ -79,13 +83,13 @@ public class HashMapStateProvider implements StateProvider {
existing = committedStates.get(componentId);
}
return existing == null ? new StandardStateMap(Collections.emptyMap(), -1) : existing;
return existing == null ? new StandardStateMap(Collections.emptyMap(), Optional.empty()) : existing;
}
@Override
public synchronized boolean replace(final StateMap oldValue, final Map<String, String> newValue, final String componentId) {
final StateMap existing = getState(componentId);
if (oldValue.getVersion() == existing.getVersion() && oldValue.toMap().equals(existing.toMap())) {
if (oldValue.getStateVersion().equals(existing.getStateVersion()) && oldValue.toMap().equals(existing.toMap())) {
setState(newValue, componentId);
return true;
}
@ -148,4 +152,10 @@ public class HashMapStateProvider implements StateProvider {
public String getIdentifier() {
return "stateless-state-provider";
}
private String getIncrementedVersion(final String currentVersion) {
final long versionNumber = Long.parseLong(currentVersion);
final long version = versionNumber + VERSION_INCREMENT;
return String.valueOf(version);
}
}

View File

@ -667,14 +667,13 @@ public class StandardStatelessFlow implements StatelessDataflow {
for (final Map.Entry<String, StateMap> entry : stateMaps.entrySet()) {
final String componentId = entry.getKey();
final StateMap stateMap = entry.getValue();
if (stateMap.getVersion() == -1) {
// Version of -1 indicates no state has been stored.
if (!stateMap.getStateVersion().isPresent()) {
continue;
}
final SerializableStateMap serializableStateMap = new SerializableStateMap();
serializableStateMap.setStateValues(stateMap.toMap());
serializableStateMap.setVersion(stateMap.getVersion());
serializableStateMap.setVersion(stateMap.getStateVersion().orElse(null));
final String serialized;
try {
@ -716,7 +715,7 @@ public class StandardStatelessFlow implements StatelessDataflow {
continue;
}
final StateMap stateMap = new StandardStateMap(deserialized.getStateValues(), deserialized.getVersion());
final StateMap stateMap = new StandardStateMap(deserialized.getStateValues(), Optional.ofNullable(deserialized.getVersion()));
deserializedStateMaps.put(componentId, stateMap);
}
@ -779,14 +778,14 @@ public class StandardStatelessFlow implements StatelessDataflow {
}
private static class SerializableStateMap {
private long version;
private String version;
private Map<String, String> stateValues;
public long getVersion() {
public String getVersion() {
return version;
}
public void setVersion(final long version) {
public void setVersion(final String version) {
this.version = version;
}

16
pom.xml
View File

@ -106,6 +106,7 @@
<com.amazonaws.version>1.12.371</com.amazonaws.version>
<software.amazon.awssdk.version>2.17.295</software.amazon.awssdk.version>
<gson.version>2.10.1</gson.version>
<io.fabric8.kubernetes.client.version>6.3.1</io.fabric8.kubernetes.client.version>
<kotlin.version>1.8.10</kotlin.version>
<okhttp.version>4.10.0</okhttp.version>
<org.apache.commons.cli.version>1.5.0</org.apache.commons.cli.version>
@ -588,6 +589,21 @@
<artifactId>swagger-annotations</artifactId>
<version>${swagger.annotations.version}</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client-api</artifactId>
<version>${io.fabric8.kubernetes.client.version}</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>${io.fabric8.kubernetes.client.version}</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-server-mock</artifactId>
<version>${io.fabric8.kubernetes.client.version}</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>