/* * 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.cassandra.locator; import java.io.IOException; import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; 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 com.google.common.net.InetAddresses; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.RandomPartitioner; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.gms.ApplicationState; import org.apache.cassandra.gms.Gossiper; import org.apache.cassandra.gms.VersionedValue; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; /** * Unit tests for {@link PropertyFileSnitch}. */ public class PropertyFileSnitchTest { private Path effectiveFile; private Path backupFile; private VersionedValue.VersionedValueFactory valueFactory; private Map<InetAddress, Set<Token>> tokenMap; @BeforeClass public static void setupDD() { DatabaseDescriptor.daemonInitialization(); } @Before public void setup() throws ConfigurationException, IOException { String confFile = FBUtilities.resourceToFile(PropertyFileSnitch.SNITCH_PROPERTIES_FILENAME); effectiveFile = Paths.get(confFile); backupFile = Paths.get(confFile + ".bak"); restoreOrigConfigFile(); InetAddress[] hosts = { InetAddress.getByName("127.0.0.1"), // this exists in the config file InetAddress.getByName("127.0.0.2"), // this exists in the config file InetAddress.getByName("127.0.0.9"), // this does not exist in the config file }; IPartitioner partitioner = new RandomPartitioner(); valueFactory = new VersionedValue.VersionedValueFactory(partitioner); tokenMap = new HashMap<>(); for (InetAddress host : hosts) { Set<Token> tokens = Collections.singleton(partitioner.getRandomToken()); Gossiper.instance.initializeNodeUnsafe(host, UUID.randomUUID(), 1); Gossiper.instance.injectApplicationState(host, ApplicationState.TOKENS, valueFactory.tokens(tokens)); setNodeShutdown(host); tokenMap.put(host, tokens); } } private void restoreOrigConfigFile() throws IOException { if (Files.exists(backupFile)) { Files.copy(backupFile, effectiveFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); Files.delete(backupFile); } } private void replaceConfigFile(Map<String, String> replacements) throws IOException { List<String> lines = Files.readAllLines(effectiveFile, StandardCharsets.UTF_8); List<String> newLines = new ArrayList<>(lines.size()); Set<String> replaced = new HashSet<>(); for (String line : lines) { String[] info = line.split("="); if (info.length == 2 && replacements.containsKey(info[0])) { String replacement = replacements.get(info[0]); if (!replacement.isEmpty()) // empty means remove this line newLines.add(info[0] + '=' + replacement); replaced.add(info[0]); } else { newLines.add(line); } } // add any new lines that were not replaced for (Map.Entry<String, String> replacement : replacements.entrySet()) { if (replaced.contains(replacement.getKey())) continue; if (!replacement.getValue().isEmpty()) // empty means remove this line so do nothing here newLines.add(replacement.getKey() + '=' + replacement.getValue()); } Files.write(effectiveFile, newLines, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING); } private void setNodeShutdown(InetAddress host) { StorageService.instance.getTokenMetadata().removeEndpoint(host); Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS, valueFactory.shutdown(true)); Gossiper.instance.markDead(host, Gossiper.instance.getEndpointStateForEndpoint(host)); } private void setNodeLive(InetAddress host) { Gossiper.instance.injectApplicationState(host, ApplicationState.STATUS, valueFactory.normal(tokenMap.get(host))); Gossiper.instance.realMarkAlive(host, Gossiper.instance.getEndpointStateForEndpoint(host)); StorageService.instance.getTokenMetadata().updateNormalTokens(tokenMap.get(host), host); } private static void checkEndpoint(final AbstractNetworkTopologySnitch snitch, final String endpointString, final String expectedDatacenter, final String expectedRack) { final InetAddress endpoint = InetAddresses.forString(endpointString); assertEquals(expectedDatacenter, snitch.getDatacenter(endpoint)); assertEquals(expectedRack, snitch.getRack(endpoint)); } /** * Test that changing rack for a host in the configuration file is only effective if the host is not live. * The original configuration file contains: 127.0.0.1=DC1:RAC1 */ @Test public void testChangeHostRack() throws Exception { final InetAddress host = InetAddress.getByName("127.0.0.1"); final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC1"); try { setNodeLive(host); Files.copy(effectiveFile, backupFile); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC1:RAC2")); Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC1"); setNodeShutdown(host); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC1:RAC2")); Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC2"); } finally { restoreOrigConfigFile(); setNodeShutdown(host); } } /** * Test that changing dc for a host in the configuration file is only effective if the host is not live. * The original configuration file contains: 127.0.0.1=DC1:RAC1 */ @Test public void testChangeHostDc() throws Exception { final InetAddress host = InetAddress.getByName("127.0.0.1"); final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC1"); try { setNodeLive(host); Files.copy(effectiveFile, backupFile); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC2:RAC1")); Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC1"); setNodeShutdown(host); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC2:RAC1")); Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC2", "RAC1"); } finally { restoreOrigConfigFile(); setNodeShutdown(host); } } /** * Test that adding a host to the configuration file changes the host dc and rack only if the host * is not live. The original configuration file does not contain 127.0.0.9 and so it should use * the default default=DC1:r1. */ @Test public void testAddHost() throws Exception { final InetAddress host = InetAddress.getByName("127.0.0.9"); final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); checkEndpoint(snitch, host.getHostAddress(), "DC1", "r1"); // default try { setNodeLive(host); Files.copy(effectiveFile, backupFile); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC2:RAC2")); // add this line if not yet there Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "r1"); // unchanged setNodeShutdown(host); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "DC2:RAC2")); // add this line if not yet there Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC2", "RAC2"); // changed } finally { restoreOrigConfigFile(); setNodeShutdown(host); } } /** * Test that removing a host from the configuration file changes the host rack only if the host * is not live. The original configuration file contains 127.0.0.2=DC1:RAC2 and default=DC1:r1 so removing * this host should result in a different rack if the host is not live. */ @Test public void testRemoveHost() throws Exception { final InetAddress host = InetAddress.getByName("127.0.0.2"); final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC2"); try { setNodeLive(host); Files.copy(effectiveFile, backupFile); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "")); // removes line if found Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "RAC2"); // unchanged setNodeShutdown(host); replaceConfigFile(Collections.singletonMap(host.getHostAddress(), "")); // removes line if found Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "r1"); // default } finally { restoreOrigConfigFile(); setNodeShutdown(host); } } /** * Test that we can change the default only if this does not result in any live node changing dc or rack. * The configuration file contains default=DC1:r1 and we change it to default=DC2:r2. Let's use host 127.0.0.9 * since it is not in the configuration file. */ @Test public void testChangeDefault() throws Exception { final InetAddress host = InetAddress.getByName("127.0.0.9"); final PropertyFileSnitch snitch = new PropertyFileSnitch(/*refreshPeriodInSeconds*/1); checkEndpoint(snitch, host.getHostAddress(), "DC1", "r1"); // default try { setNodeLive(host); Files.copy(effectiveFile, backupFile); replaceConfigFile(Collections.singletonMap("default", "DC2:r2")); // change default Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC1", "r1"); // unchanged setNodeShutdown(host); replaceConfigFile(Collections.singletonMap("default", "DC2:r2")); // change default again (refresh file update) Thread.sleep(1500); checkEndpoint(snitch, host.getHostAddress(), "DC2", "r2"); // default updated } finally { restoreOrigConfigFile(); setNodeShutdown(host); } } }