| /* |
| * 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.db.guardrails; |
| |
| import java.io.IOException; |
| import java.net.UnknownHostException; |
| import java.nio.file.FileStore; |
| import java.util.Arrays; |
| import java.util.function.Consumer; |
| |
| import com.google.common.collect.HashMultimap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Multimap; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.apache.cassandra.config.DataStorageSpec; |
| import org.apache.cassandra.db.ConsistencyLevel; |
| import org.apache.cassandra.db.Directories; |
| import org.apache.cassandra.gms.ApplicationState; |
| import org.apache.cassandra.gms.Gossiper; |
| import org.apache.cassandra.gms.VersionedValue; |
| import org.apache.cassandra.io.util.FileUtils; |
| import org.apache.cassandra.locator.InetAddressAndPort; |
| import org.apache.cassandra.service.ClientState; |
| import org.apache.cassandra.service.StorageService; |
| import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster; |
| import org.apache.cassandra.service.disk.usage.DiskUsageMonitor; |
| import org.apache.cassandra.service.disk.usage.DiskUsageState; |
| import org.apache.cassandra.utils.FBUtilities; |
| import org.jboss.byteman.contrib.bmunit.BMRule; |
| import org.jboss.byteman.contrib.bmunit.BMUnitRunner; |
| import org.mockito.Mockito; |
| |
| import static org.apache.cassandra.service.disk.usage.DiskUsageState.FULL; |
| import static org.apache.cassandra.service.disk.usage.DiskUsageState.NOT_AVAILABLE; |
| import static org.apache.cassandra.service.disk.usage.DiskUsageState.SPACIOUS; |
| import static org.apache.cassandra.service.disk.usage.DiskUsageState.STUFFED; |
| import static org.assertj.core.api.Assertions.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.mockito.Mockito.doCallRealMethod; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.when; |
| |
| /** |
| * Tests the guardrails for disk usage, {@link Guardrails#localDataDiskUsage} and {@link Guardrails#replicaDiskUsage}. |
| */ |
| @RunWith(BMUnitRunner.class) |
| @BMRule(name = "Always returns a physical disk size of 1000TiB", |
| targetClass = "DiskUsageMonitor", |
| targetMethod = "totalDiskSpace", |
| action = "return " + (1000L * 1024 * 1024 * 1024 * 1024) + "L;") // 1000TiB |
| public class GuardrailDiskUsageTest extends GuardrailTester |
| { |
| private static int defaultDataDiskUsagePercentageWarnThreshold; |
| private static int defaultDataDiskUsagePercentageFailThreshold; |
| |
| @BeforeClass |
| public static void beforeClass() |
| { |
| defaultDataDiskUsagePercentageWarnThreshold = Guardrails.instance.getDataDiskUsagePercentageWarnThreshold(); |
| defaultDataDiskUsagePercentageFailThreshold = Guardrails.instance.getDataDiskUsagePercentageFailThreshold(); |
| |
| Guardrails.instance.setDataDiskUsagePercentageThreshold(-1, -1); |
| } |
| |
| @AfterClass |
| public static void afterClass() |
| { |
| Guardrails.instance.setDataDiskUsagePercentageThreshold(defaultDataDiskUsagePercentageWarnThreshold, |
| defaultDataDiskUsagePercentageFailThreshold); |
| } |
| |
| @Test |
| public void testConfigValidation() |
| { |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize(null)); |
| assertNull(guardrails().getDataDiskUsageMaxDiskSize()); |
| |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize("0B"), "0 is not allowed"); |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize("0KiB"), "0 is not allowed"); |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize("0MiB"), "0 is not allowed"); |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize("0GiB"), "0 is not allowed"); |
| |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize("10B")); |
| assertEquals("10B", guardrails().getDataDiskUsageMaxDiskSize()); |
| |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize("20KiB")); |
| assertEquals("20KiB", guardrails().getDataDiskUsageMaxDiskSize()); |
| |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize("30MiB")); |
| assertEquals("30MiB", guardrails().getDataDiskUsageMaxDiskSize()); |
| |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize("40GiB")); |
| assertEquals("40GiB", guardrails().getDataDiskUsageMaxDiskSize()); |
| |
| long diskSize = DiskUsageMonitor.totalDiskSpace(); |
| String message = String.format("only %s are actually available on disk", FileUtils.stringifyFileSize(diskSize)); |
| assertConfigValid(x -> x.setDataDiskUsageMaxDiskSize(diskSize + "B")); |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize(diskSize + 1 + "B"), message); |
| // We want to test with very big number, Long.MAX_VALUE is not allowed so it was easy to use Intger.MAX_VALUE |
| assertConfigFails(x -> x.setDataDiskUsageMaxDiskSize(Integer.MAX_VALUE + "GiB"), message); |
| |
| // warn threshold smaller than lower bound |
| assertConfigFails(x -> x.setDataDiskUsagePercentageThreshold(0, 80), "0 is not allowed"); |
| |
| // fail threshold bigger than upper bound |
| assertConfigFails(x -> x.setDataDiskUsagePercentageThreshold(1, 110), "maximum allowed value is 100"); |
| |
| // warn threshold larger than fail threshold |
| assertConfigFails(x -> x.setDataDiskUsagePercentageThreshold(60, 50), |
| "The warn threshold 60 for data_disk_usage_percentage_warn_threshold should be lower than the fail threshold 50"); |
| } |
| |
| @Test |
| public void testDiskUsageState() |
| { |
| guardrails().setDataDiskUsagePercentageThreshold(50, 90); |
| |
| // under usage |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(10)); |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(50)); |
| |
| // exceed warning threshold |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(51)); |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(56)); |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(90)); |
| |
| // exceed fail threshold |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(91)); |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(100)); |
| |
| // shouldn't be possible to go over 100% but just to be sure |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(101)); |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(500)); |
| } |
| |
| @Test |
| public void testDiskUsageDetectorWarnDisabled() |
| { |
| guardrails().setDataDiskUsagePercentageThreshold(-1, 90); |
| |
| // under usage |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(0)); |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(50)); |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(90)); |
| |
| // exceed fail threshold |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(91)); |
| assertEquals(FULL, DiskUsageMonitor.instance.getState(100)); |
| } |
| |
| @Test |
| public void testDiskUsageDetectorFailDisabled() |
| { |
| guardrails().setDataDiskUsagePercentageThreshold(50, -1); |
| |
| // under usage |
| assertEquals(SPACIOUS, DiskUsageMonitor.instance.getState(50)); |
| |
| // exceed warning threshold |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(51)); |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(80)); |
| assertEquals(STUFFED, DiskUsageMonitor.instance.getState(100)); |
| } |
| |
| @Test |
| public void testDiskUsageGuardrailDisabled() |
| { |
| guardrails().setDataDiskUsagePercentageThreshold(-1, -1); |
| |
| assertEquals(NOT_AVAILABLE, DiskUsageMonitor.instance.getState(0)); |
| assertEquals(NOT_AVAILABLE, DiskUsageMonitor.instance.getState(60)); |
| assertEquals(NOT_AVAILABLE, DiskUsageMonitor.instance.getState(100)); |
| } |
| |
| @Test |
| public void testMemtableSizeIncluded() throws Throwable |
| { |
| DiskUsageMonitor monitor = new DiskUsageMonitor(); |
| |
| createTable(keyspace(), "CREATE TABLE %s (k text PRIMARY KEY, v text) WITH compression = { 'enabled': false }"); |
| |
| long memtableSizeBefore = monitor.getAllMemtableSize(); |
| int rows = 10; |
| int mb = 1024 * 1024; |
| |
| for (int i = 0; i < rows; i++) |
| { |
| char[] chars = new char[mb]; |
| Arrays.fill(chars, (char) i); |
| String value = String.copyValueOf(chars); |
| execute("INSERT INTO %s (k, v) VALUES(?, ?)", i, value); |
| } |
| |
| // verify memtables are included |
| long memtableSizeAfterInsert = monitor.getAllMemtableSize(); |
| assertTrue(String.format("Expect at least 10MB more data, but got before: %s and after: %d", |
| memtableSizeBefore, memtableSizeAfterInsert), |
| memtableSizeAfterInsert - memtableSizeBefore >= rows * mb); |
| |
| // verify memtable size are reduced after flush |
| flush(); |
| long memtableSizeAfterFlush = monitor.getAllMemtableSize(); |
| assertEquals(memtableSizeBefore, memtableSizeAfterFlush, mb); |
| } |
| |
| @Test |
| public void testMonitorLogsOnStateChange() |
| { |
| guardrails().setDataDiskUsagePercentageThreshold(50, 90); |
| |
| Guardrails.localDataDiskUsage.resetLastNotifyTime(); |
| |
| DiskUsageMonitor monitor = new DiskUsageMonitor(); |
| |
| // transit to SPACIOUS, no logging |
| assertMonitorStateTransition(0.50, SPACIOUS, monitor); |
| |
| // transit to STUFFED, expect warning |
| assertMonitorStateTransition(0.50001, STUFFED, monitor, true, "Local data disk usage 51%(Stuffed) exceeds warning threshold of 50%"); |
| |
| // remain as STUFFED, no logging because of min log interval |
| assertMonitorStateTransition(0.90, STUFFED, monitor); |
| |
| // transit to FULL, expect failure |
| assertMonitorStateTransition(0.90001, FULL, monitor, false, "Local data disk usage 91%(Full) exceeds failure threshold of 90%, will stop accepting writes"); |
| |
| // remain as FULL, no logging because of min log interval |
| assertMonitorStateTransition(0.99, FULL, monitor); |
| |
| // remain as FULL, no logging because of min log interval |
| assertMonitorStateTransition(5.0, FULL, monitor); |
| |
| // transit back to STUFFED, no warning because of min log interval |
| assertMonitorStateTransition(0.90, STUFFED, monitor); |
| |
| // transit back to FULL, no logging because of min log interval |
| assertMonitorStateTransition(0.900001, FULL, monitor); |
| |
| // transit back to STUFFED, no logging because of min log interval |
| assertMonitorStateTransition(0.90, STUFFED, monitor); |
| |
| // transit to SPACIOUS, no logging |
| assertMonitorStateTransition(0.50, SPACIOUS, monitor); |
| } |
| |
| @Test |
| public void testDiskUsageBroadcaster() throws UnknownHostException |
| { |
| DiskUsageBroadcaster broadcaster = new DiskUsageBroadcaster(null); |
| Gossiper.instance.unregister(broadcaster); |
| |
| InetAddressAndPort node1 = InetAddressAndPort.getByName("127.0.0.1"); |
| InetAddressAndPort node2 = InetAddressAndPort.getByName("127.0.0.2"); |
| InetAddressAndPort node3 = InetAddressAndPort.getByName("127.0.0.3"); |
| |
| // initially it's NOT_AVAILABLE |
| assertFalse(broadcaster.hasStuffedOrFullNode()); |
| assertFalse(broadcaster.isFull(node1)); |
| assertFalse(broadcaster.isFull(node2)); |
| assertFalse(broadcaster.isFull(node3)); |
| |
| // adding 1st node: Spacious, cluster has no Full node |
| broadcaster.onChange(node1, ApplicationState.DISK_USAGE, value(SPACIOUS)); |
| assertFalse(broadcaster.hasStuffedOrFullNode()); |
| assertFalse(broadcaster.isFull(node1)); |
| |
| // adding 2nd node with wrong ApplicationState |
| broadcaster.onChange(node2, ApplicationState.RACK, value(FULL)); |
| assertFalse(broadcaster.hasStuffedOrFullNode()); |
| assertFalse(broadcaster.isFull(node2)); |
| |
| // adding 2nd node: STUFFED |
| broadcaster.onChange(node2, ApplicationState.DISK_USAGE, value(STUFFED)); |
| assertTrue(broadcaster.hasStuffedOrFullNode()); |
| assertTrue(broadcaster.isStuffed(node2)); |
| |
| // adding 3rd node: FULL |
| broadcaster.onChange(node3, ApplicationState.DISK_USAGE, value(FULL)); |
| assertTrue(broadcaster.hasStuffedOrFullNode()); |
| assertTrue(broadcaster.isFull(node3)); |
| |
| // remove 2nd node, cluster has Full node |
| broadcaster.onRemove(node2); |
| assertTrue(broadcaster.hasStuffedOrFullNode()); |
| assertFalse(broadcaster.isStuffed(node2)); |
| |
| // remove 3nd node, cluster has no Full node |
| broadcaster.onRemove(node3); |
| assertFalse(broadcaster.hasStuffedOrFullNode()); |
| assertFalse(broadcaster.isFull(node3)); |
| } |
| |
| @Test |
| public void testDiskUsageCalculationWithMaxDiskSize() throws IOException |
| { |
| Directories.DataDirectory directory = mock(Directories.DataDirectory.class); |
| when(directory.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("5GiB").toBytes()); |
| |
| FileStore store = mock(FileStore.class); |
| when(store.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound("95GiB").toBytes()); // 100GiB disk - 5GiB |
| |
| Multimap<FileStore, Directories.DataDirectory> directories = HashMultimap.create(); |
| directories.put(store, directory); |
| DiskUsageMonitor monitor = spy(new DiskUsageMonitor(() -> directories)); |
| |
| doCallRealMethod().when(monitor).getDiskUsage(); |
| doReturn(0L).when(monitor).getAllMemtableSize(); |
| |
| guardrails().setDataDiskUsageMaxDiskSize(null); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.05); |
| |
| // 5G are used of 10G |
| guardrails().setDataDiskUsageMaxDiskSize("10GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.5); |
| |
| // max disk size = space used |
| guardrails().setDataDiskUsageMaxDiskSize("5GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.0); |
| |
| // max disk size < space used |
| guardrails().setDataDiskUsageMaxDiskSize("1GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(5.0); |
| } |
| |
| @Test |
| public void testDiskUsageCalculationWithMaxDiskSizeAndSmallUnits() throws IOException |
| { |
| // 5GiB used out of 100GiB disk |
| long freeDiskSizeInBytes = new DataStorageSpec.LongBytesBound("100GiB").toBytes() - new DataStorageSpec.LongBytesBound("5MiB").toBytes(); |
| |
| FileStore store = mock(FileStore.class); |
| when(store.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound(freeDiskSizeInBytes + "B").toBytes()); // 100GiB disk |
| |
| Directories.DataDirectory directory = mock(Directories.DataDirectory.class); |
| when(directory.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("5MiB").toBytes()); |
| |
| Multimap<FileStore, Directories.DataDirectory> directories = HashMultimap.create(); |
| directories.put(store, directory); |
| DiskUsageMonitor monitor = spy(new DiskUsageMonitor(() -> directories)); |
| |
| doCallRealMethod().when(monitor).getDiskUsage(); |
| doReturn(0L).when(monitor).getAllMemtableSize(); |
| |
| guardrails().setDataDiskUsageMaxDiskSize(null); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.00005); |
| |
| // 5MiB are used of 10MiB |
| guardrails().setDataDiskUsageMaxDiskSize("10MiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.5); |
| |
| // max disk size = space used |
| guardrails().setDataDiskUsageMaxDiskSize("5MiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.0); |
| |
| // max disk size < space used |
| guardrails().setDataDiskUsageMaxDiskSize("1MiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(5.0); |
| } |
| |
| @Test |
| public void testDiskUsageCalculationWithMaxDiskSizeAndMultipleVolumes() throws IOException |
| { |
| Mockito.reset(); |
| |
| Multimap<FileStore, Directories.DataDirectory> directories = HashMultimap.create(); |
| |
| Directories.DataDirectory directory1 = mock(Directories.DataDirectory.class); |
| FileStore store1 = mock(FileStore.class); |
| when(directory1.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("5GiB").toBytes()); |
| when(store1.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound("95GiB").toBytes()); // 100 GiB disk - 5 GiB |
| directories.put(store1, directory1); |
| |
| Directories.DataDirectory directory2 = mock(Directories.DataDirectory.class); |
| FileStore store2 = mock(FileStore.class); |
| when(directory2.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("25GiB").toBytes()); |
| when(store2.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound("75GiB").toBytes()); // 100 GiB disk - 25 GiB |
| directories.put(store2, directory2); |
| |
| Directories.DataDirectory directory3 = mock(Directories.DataDirectory.class); |
| FileStore store3 = mock(FileStore.class); |
| when(directory3.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("20GiB").toBytes()); |
| when(store3.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound("80GiB").toBytes()); // 100 GiB disk - 20 GiB |
| directories.put(store3, directory3); |
| |
| DiskUsageMonitor monitor = spy(new DiskUsageMonitor(() -> directories)); |
| |
| doCallRealMethod().when(monitor).getDiskUsage(); |
| doReturn(0L).when(monitor).getAllMemtableSize(); |
| |
| // 50G/300G as each disk has a capacity of 100G |
| guardrails().setDataDiskUsageMaxDiskSize(null); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.16667); |
| |
| // 50G/100G |
| guardrails().setDataDiskUsageMaxDiskSize("100GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.5); |
| |
| // 50G/75G |
| guardrails().setDataDiskUsageMaxDiskSize("75GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.66667); |
| |
| // 50G/50G |
| guardrails().setDataDiskUsageMaxDiskSize("50GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.0); |
| |
| // 50G/49G |
| guardrails().setDataDiskUsageMaxDiskSize("49GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.02041); |
| } |
| |
| @Test |
| public void testDiskUsageCalculationWithMaxDiskSizeAndMultipleDirectories() throws IOException |
| { |
| Mockito.reset(); |
| |
| Directories.DataDirectory directory1 = mock(Directories.DataDirectory.class); |
| when(directory1.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("5GiB").toBytes()); |
| |
| Directories.DataDirectory directory2 = mock(Directories.DataDirectory.class); |
| when(directory2.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("25GiB").toBytes()); |
| |
| Directories.DataDirectory directory3 = mock(Directories.DataDirectory.class); |
| when(directory3.getRawSize()).thenReturn(new DataStorageSpec.LongBytesBound("20GiB").toBytes()); |
| |
| FileStore store = mock(FileStore.class); |
| when(store.getUsableSpace()).thenReturn(new DataStorageSpec.LongBytesBound("250GiB").toBytes()); // 100 GiB disk (300 - 5 - 25 - 20) |
| |
| Multimap<FileStore, Directories.DataDirectory> directories = HashMultimap.create(); |
| directories.putAll(store, ImmutableSet.of(directory1, directory2, directory3)); |
| |
| DiskUsageMonitor monitor = spy(new DiskUsageMonitor(() -> directories)); |
| |
| doCallRealMethod().when(monitor).getDiskUsage(); |
| doReturn(0L).when(monitor).getAllMemtableSize(); |
| |
| // 50G/300G as disk has a capacity of 300G |
| guardrails().setDataDiskUsageMaxDiskSize(null); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.16667); |
| |
| // 50G/100G |
| guardrails().setDataDiskUsageMaxDiskSize("100GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.5); |
| |
| // 50G/75G |
| guardrails().setDataDiskUsageMaxDiskSize("75GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(0.66667); |
| |
| // 50G/50G |
| guardrails().setDataDiskUsageMaxDiskSize("50GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.0); |
| |
| // 50G/49G |
| guardrails().setDataDiskUsageMaxDiskSize("49GiB"); |
| assertThat(monitor.getDiskUsage()).isEqualTo(1.02041); |
| } |
| |
| @Test |
| public void testWriteRequests() throws Throwable |
| { |
| createTable("CREATE TABLE %s (k int PRIMARY KEY, v int)"); |
| |
| InetAddressAndPort local = FBUtilities.getBroadcastAddressAndPort(); |
| InetAddressAndPort node1 = InetAddressAndPort.getByName("127.0.0.11"); |
| InetAddressAndPort node2 = InetAddressAndPort.getByName("127.0.0.21"); |
| InetAddressAndPort node3 = InetAddressAndPort.getByName("127.0.0.31"); |
| |
| Guardrails.replicaDiskUsage.resetLastNotifyTime(); |
| guardrails().setDataDiskUsagePercentageThreshold(98, 99); |
| |
| ConsistencyLevel cl = ConsistencyLevel.LOCAL_QUORUM; |
| String select = "SELECT * FROM %s"; |
| String insert = "INSERT INTO %s (k, v) VALUES (0, 0)"; |
| String batch = "BEGIN BATCH " + |
| "INSERT INTO %s (k, v) VALUES (1, 1);" + |
| "INSERT INTO %<s (k, v) VALUES (2, 2); " + |
| "APPLY BATCH"; |
| CheckedFunction userSelect = () -> execute(userClientState, select, cl); |
| CheckedFunction userInsert = () -> execute(userClientState, insert, cl); |
| CheckedFunction userBatch = () -> execute(userClientState, batch, cl); |
| |
| // default state, write request works fine |
| assertValid(userSelect); |
| assertValid(userInsert); |
| assertValid(userBatch); |
| |
| // verify node1 NOT_AVAILABLE won't affect writes |
| setDiskUsageState(node1, NOT_AVAILABLE); |
| assertValid(userSelect); |
| assertValid(userInsert); |
| assertValid(userBatch); |
| |
| // verify node2 Spacious won't affect writes |
| setDiskUsageState(node2, SPACIOUS); |
| assertValid(userSelect); |
| assertValid(userInsert); |
| assertValid(userBatch); |
| |
| // verify node3 STUFFED won't trigger warning as it's not write replica |
| setDiskUsageState(node3, STUFFED); |
| assertValid(userSelect); |
| assertValid(userInsert); |
| assertValid(userBatch); |
| |
| // verify node3 Full won't affect writes as it's not write replica |
| setDiskUsageState(node3, FULL); |
| assertValid(userSelect); |
| assertValid(userInsert); |
| assertValid(userBatch); |
| |
| // verify local node STUFF, will log warning |
| setDiskUsageState(local, STUFFED); |
| assertValid(userSelect); |
| assertWarns(userInsert); |
| assertWarns(userBatch); |
| |
| // verify local node Full, will reject writes |
| setDiskUsageState(local, FULL); |
| assertValid(userSelect); |
| assertFails(userInsert); |
| assertFails(userBatch); |
| |
| // excluded users can write to FULL cluster |
| useSuperUser(); |
| Guardrails.replicaDiskUsage.resetLastNotifyTime(); |
| for (ClientState excludedUser : Arrays.asList(systemClientState, superClientState)) |
| { |
| assertValid(() -> execute(excludedUser, select, cl)); |
| assertValid(() -> execute(excludedUser, insert, cl)); |
| assertValid(() -> execute(excludedUser, batch, cl)); |
| } |
| |
| // verify local node STUFFED won't reject writes |
| setDiskUsageState(local, STUFFED); |
| assertValid(userSelect); |
| assertWarns(userInsert); |
| assertWarns(userBatch); |
| } |
| |
| @Override |
| protected void assertValid(CheckedFunction function) throws Throwable |
| { |
| Guardrails.replicaDiskUsage.resetLastNotifyTime(); |
| super.assertValid(function); |
| } |
| |
| protected void assertWarns(CheckedFunction function) throws Throwable |
| { |
| Guardrails.replicaDiskUsage.resetLastNotifyTime(); |
| super.assertWarns(function, "Replica disk usage exceeds warning threshold"); |
| } |
| |
| protected void assertFails(CheckedFunction function) throws Throwable |
| { |
| Guardrails.replicaDiskUsage.resetLastNotifyTime(); |
| super.assertFails(function, "Write request failed because disk usage exceeds failure threshold"); |
| } |
| |
| private static void setDiskUsageState(InetAddressAndPort endpoint, DiskUsageState state) |
| { |
| DiskUsageBroadcaster.instance.onChange(endpoint, ApplicationState.DISK_USAGE, value(state)); |
| } |
| |
| private static VersionedValue value(DiskUsageState state) |
| { |
| return StorageService.instance.valueFactory.diskUsage(state.name()); |
| } |
| |
| private void assertMonitorStateTransition(double usageRatio, DiskUsageState state, DiskUsageMonitor monitor) |
| { |
| assertMonitorStateTransition(usageRatio, state, monitor, false, null); |
| } |
| |
| private void assertMonitorStateTransition(double usageRatio, DiskUsageState state, DiskUsageMonitor monitor, |
| boolean isWarn, String msg) |
| { |
| boolean stateChanged = state != monitor.state(); |
| Consumer<DiskUsageState> notifier = newState -> { |
| if (stateChanged) |
| assertEquals(state, newState); |
| else |
| fail("Expect no notification if state remains the same"); |
| }; |
| |
| monitor.updateLocalState(usageRatio, notifier); |
| assertEquals(state, monitor.state()); |
| |
| if (msg == null) |
| { |
| listener.assertNotFailed(); |
| listener.assertNotWarned(); |
| } |
| else if (isWarn) |
| { |
| listener.assertWarned(msg); |
| listener.assertNotFailed(); |
| } |
| else |
| { |
| listener.assertFailed(msg); |
| listener.assertNotWarned(); |
| } |
| |
| listener.clear(); |
| } |
| } |