/*
* 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.heartbeat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.nifi.cluster.ReportedEvent;
import org.apache.nifi.cluster.coordination.ClusterCoordinator;
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.coordination.node.NodeWorkload;
import org.apache.nifi.cluster.event.NodeEvent;
import org.apache.nifi.cluster.protocol.NodeIdentifier;
import org.apache.nifi.reporting.Severity;
import org.apache.nifi.services.FlowService;
import org.apache.nifi.util.NiFiProperties;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestAbstractHeartbeatMonitor {
private NodeIdentifier nodeId;
private TestFriendlyHeartbeatMonitor monitor;
@Before
public void setup() throws Exception {
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, "src/test/resources/conf/nifi.properties");
nodeId = new NodeIdentifier(UUID.randomUUID().toString(), "localhost", 9999, "localhost", 8888, "localhost", null, null, false);
}
@After
public void clear() throws IOException {
if (monitor != null) {
monitor.stop();
}
}
/**
* Verifies that a node that sends a heartbeat that indicates that it is 'connected' is asked to connect to
* cluster if the cluster coordinator does not know about the node
*
* @throws InterruptedException if interrupted
*/
@Test
public void testNewConnectedHeartbeatFromUnknownNode() throws IOException, InterruptedException {
final List<NodeIdentifier> requestedToConnect = Collections.synchronizedList(new ArrayList<>());
final ClusterCoordinatorAdapter coordinator = new ClusterCoordinatorAdapter() {
@Override
public synchronized void requestNodeConnect(final NodeIdentifier nodeId, String userDn) {
requestedToConnect.add(nodeId);
}
@Override
public boolean isActiveClusterCoordinator() {
return true;
}
};
final TestFriendlyHeartbeatMonitor monitor = createMonitor(coordinator);
// Ensure that we request the Unknown Node connect to the cluster
final NodeHeartbeat heartbeat = createHeartbeat(nodeId, NodeConnectionState.CONNECTED);
monitor.addHeartbeat(heartbeat);
monitor.waitForProcessed();
assertEquals(1, requestedToConnect.size());
assertEquals(nodeId, requestedToConnect.get(0));
assertEquals(1, coordinator.getEvents().size());
}
/**
* Verifies that a node that sends a heartbeat that indicates that it is 'connected' if previously
* manually disconnected, will be asked to disconnect from the cluster again.
*
* @throws InterruptedException if interrupted
*/
@Test
public void testHeartbeatFromManuallyDisconnectedNode() throws InterruptedException {
final Set<NodeIdentifier> requestedToConnect = Collections.synchronizedSet(new HashSet<>());
final Set<NodeIdentifier> requestedToDisconnect = Collections.synchronizedSet(new HashSet<>());
final ClusterCoordinatorAdapter adapter = new ClusterCoordinatorAdapter() {
@Override
public synchronized void requestNodeConnect(final NodeIdentifier nodeId) {
super.requestNodeConnect(nodeId);
requestedToConnect.add(nodeId);
}
@Override
public synchronized void requestNodeDisconnect(final NodeIdentifier nodeId, final DisconnectionCode disconnectionCode, final String explanation) {
super.requestNodeDisconnect(nodeId, disconnectionCode, explanation);
requestedToDisconnect.add(nodeId);
}
};
final TestFriendlyHeartbeatMonitor monitor = createMonitor(adapter);
adapter.requestNodeDisconnect(nodeId, DisconnectionCode.USER_DISCONNECTED, "Unit Testing");
monitor.addHeartbeat(createHeartbeat(nodeId, NodeConnectionState.CONNECTED));
monitor.waitForProcessed();
assertEquals(1, requestedToDisconnect.size());
assertEquals(nodeId, requestedToDisconnect.iterator().next());
assertTrue(requestedToConnect.isEmpty());
}
@Test
public void testConnectingNodeMarkedConnectedWhenHeartbeatReceived() throws InterruptedException {
final Set<NodeIdentifier> requestedToConnect = Collections.synchronizedSet(new HashSet<>());
final Set<NodeIdentifier> connected = Collections.synchronizedSet(new HashSet<>());
final ClusterCoordinatorAdapter adapter = new ClusterCoordinatorAdapter() {
@Override
public synchronized void requestNodeConnect(final NodeIdentifier nodeId) {
super.requestNodeConnect(nodeId);
requestedToConnect.add(nodeId);
}
@Override
public synchronized void finishNodeConnection(final NodeIdentifier nodeId) {
super.finishNodeConnection(nodeId);
connected.add(nodeId);
}
@Override
public boolean isActiveClusterCoordinator() {
return true;
}
};
final TestFriendlyHeartbeatMonitor monitor = createMonitor(adapter);
adapter.requestNodeConnect(nodeId); // set state to 'connecting'
requestedToConnect.clear();
monitor.addHeartbeat(createHeartbeat(nodeId, NodeConnectionState.CONNECTED));
monitor.waitForProcessed();
assertEquals(1, connected.size());
assertEquals(nodeId, connected.iterator().next());
assertTrue(requestedToConnect.isEmpty());
}
private NodeHeartbeat createHeartbeat(final NodeIdentifier nodeId, final NodeConnectionState state) {
final NodeConnectionStatus status = new NodeConnectionStatus(nodeId, state);
return new StandardNodeHeartbeat(nodeId, System.currentTimeMillis(), status, 0, 0, 0, 0);
}
private TestFriendlyHeartbeatMonitor createMonitor(final ClusterCoordinator coordinator) {
monitor = new TestFriendlyHeartbeatMonitor(coordinator, createProperties());
monitor.start();
return monitor;
}
private NiFiProperties createProperties() {
final Map<String, String> addProps = new HashMap<>();
addProps.put(NiFiProperties.CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL, "10 ms");
return NiFiProperties.createBasicNiFiProperties(null, addProps);
}
private static class ClusterCoordinatorAdapter implements ClusterCoordinator {
private final Map<NodeIdentifier, NodeConnectionStatus> statuses = new HashMap<>();
private final List<ReportedEvent> events = new ArrayList<>();
public synchronized void requestNodeConnect(NodeIdentifier nodeId) {
requestNodeConnect(nodeId, null);
}
@Override
public synchronized void requestNodeConnect(NodeIdentifier nodeId, String userDn) {
statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTING));
}
@Override
public void removeNode(NodeIdentifier nodeId, String userDn) {
statuses.remove(nodeId);
}
@Override
public synchronized void finishNodeConnection(NodeIdentifier nodeId) {
statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.CONNECTED));
}
@Override
public synchronized void requestNodeDisconnect(NodeIdentifier nodeId, DisconnectionCode disconnectionCode, String explanation) {
statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.DISCONNECTED));
}
@Override
public synchronized void disconnectionRequestedByNode(NodeIdentifier nodeId, DisconnectionCode disconnectionCode, String explanation) {
statuses.put(nodeId, new NodeConnectionStatus(nodeId, NodeConnectionState.DISCONNECTED));
}
@Override
public synchronized NodeConnectionStatus getConnectionStatus(NodeIdentifier nodeId) {
return statuses.get(nodeId);
}
@Override
public synchronized Set<NodeIdentifier> getNodeIdentifiers(NodeConnectionState... states) {
final Set<NodeConnectionState> stateSet = new HashSet<>();
for (final NodeConnectionState state : states) {
stateSet.add(state);
}
return statuses.entrySet().stream()
.filter(p -> stateSet.contains(p.getValue().getState()))
.map(p -> p.getKey())
.collect(Collectors.toSet());
}
@Override
public synchronized boolean isBlockedByFirewall(String hostname) {
return false;
}
@Override
public synchronized void reportEvent(NodeIdentifier nodeId, Severity severity, String event) {
events.add(new ReportedEvent(nodeId, severity, event));
}
synchronized List<ReportedEvent> getEvents() {
return new ArrayList<>(events);
}
@Override
public NodeIdentifier getNodeIdentifier(final String uuid) {
return statuses.keySet().stream().filter(p -> p.getId().equals(uuid)).findFirst().orElse(null);
}
@Override
public Map<NodeConnectionState, List<NodeIdentifier>> getConnectionStates() {
return statuses.keySet().stream().collect(Collectors.groupingBy(nodeId -> getConnectionStatus(nodeId).getState()));
}
@Override
public List<NodeEvent> getNodeEvents(NodeIdentifier nodeId) {
return null;
}
@Override
public NodeIdentifier getPrimaryNode() {
return null;
}
@Override
public void setFlowService(FlowService flowService) {
}
@Override
public void resetNodeStatuses(Map<NodeIdentifier, NodeConnectionStatus> statusMap) {
}
@Override
public void shutdown() {
}
@Override
public void setLocalNodeIdentifier(NodeIdentifier nodeId) {
}
@Override
public boolean isConnected() {
return false;
}
@Override
public void setConnected(boolean connected) {
}
@Override
public NodeIdentifier getElectedActiveCoordinatorNode() {
return null;
}
@Override
public boolean isActiveClusterCoordinator() {
return false;
}
@Override
public NodeIdentifier getLocalNodeIdentifier() {
return null;
}
@Override
public List<NodeConnectionStatus> getConnectionStatuses() {
return Collections.emptyList();
}
@Override
public boolean resetNodeStatus(NodeConnectionStatus connectionStatus, long qualifyingUpdateId) {
return false;
}
@Override
public boolean isFlowElectionComplete() {
return true;
}
@Override
public String getFlowElectionStatus() {
return null;
}
@Override
public Map<NodeIdentifier, NodeWorkload> getClusterWorkload() throws IOException {
return null;
}
}
private static class TestFriendlyHeartbeatMonitor extends AbstractHeartbeatMonitor {
private Map<NodeIdentifier, NodeHeartbeat> heartbeats = new HashMap<>();
private final Object mutex = new Object();
public TestFriendlyHeartbeatMonitor(ClusterCoordinator clusterCoordinator, NiFiProperties nifiProperties) {
super(clusterCoordinator, nifiProperties);
}
@Override
protected synchronized Map<NodeIdentifier, NodeHeartbeat> getLatestHeartbeats() {
return heartbeats;
}
@Override
public synchronized void monitorHeartbeats() {
super.monitorHeartbeats();
synchronized (mutex) {
mutex.notify();
}
}
synchronized void addHeartbeat(final NodeHeartbeat heartbeat) {
heartbeats.put(heartbeat.getNodeIdentifier(), heartbeat);
}
@Override
public synchronized void removeHeartbeat(final NodeIdentifier nodeId) {
heartbeats.remove(nodeId);
}
@Override
public synchronized void purgeHeartbeats() {
heartbeats.clear();
}
void waitForProcessed() throws InterruptedException {
synchronized (mutex) {
mutex.wait();
}
}
@Override
public String getHeartbeatAddress() {
return "localhost";
}
}
}