blob: 8a4a5801db3b44d9d4b52fefe073ebc6093ac38d [file] [log] [blame]
/*
* 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.geode.metrics;
import static java.io.File.pathSeparatorChar;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.stream.Collectors.toList;
import static org.apache.geode.cache.execute.FunctionService.onServer;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_MANAGER;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPorts;
import static org.apache.geode.test.compiler.ClassBuilder.writeJarFromClasses;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import io.micrometer.core.instrument.Timer;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.rules.ServiceJarRule;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.NotAuthorizedException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.test.junit.rules.gfsh.GfshRule;
public class CacheGetsTimerTest {
private int locatorPort;
private ClientCache clientCache;
private Region<Object, Object> replicateRegion;
private Region<Object, Object> partitionRegion;
@Rule
public GfshRule gfshRule = new GfshRule();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public ServiceJarRule serviceJarRule = new ServiceJarRule();
@After
public void tearDown() {
if (clientCache != null) {
clientCache.close();
}
if (locatorPort != 0) {
String connectToLocatorCommand = "connect --locator=localhost[" + locatorPort + "]";
String shutdownCommand = "shutdown --include-locators";
gfshRule.execute(connectToLocatorCommand, shutdownCommand);
}
}
@Test
public void hitTimerRecordsCountAndTotalTime_ifGetsPerformedOnReplicateRegionWithExistingKey()
throws IOException {
givenClusterWithTimeStatisticsEnabled();
for (int i = 0; i < 5; i++) {
replicateRegion.put(i, i);
replicateRegion.get(i);
}
TimerValue hitTimerValue = hitTimerValueForRegion(replicateRegion);
assertThat(hitTimerValue.region)
.as("Cache gets hit timer region tag value")
.isEqualTo(replicateRegion.getName());
assertThat(hitTimerValue.result)
.as("Cache gets hit timer result tag value")
.isEqualTo("hit");
assertThat(hitTimerValue.count)
.as("Cache gets hit timer count for region " + replicateRegion.getName())
.isEqualTo(5);
assertThat(hitTimerValue.totalTime)
.as("Cache gets hit timer total time for region " + replicateRegion.getName())
.isGreaterThan(0);
}
@Test
public void missTimerRecordsCountAndTotalTime_ifGetsPerformedOnReplicateRegionWithMissingKey()
throws IOException {
givenClusterWithTimeStatisticsEnabled();
for (int i = 0; i < 5; i++) {
replicateRegion.get(i);
}
TimerValue missTimerValue = missTimerValueForRegion(replicateRegion);
assertThat(missTimerValue.region)
.as("Cache gets miss timer region tag value")
.isEqualTo(replicateRegion.getName());
assertThat(missTimerValue.result)
.as("Cache gets miss timer result tag value")
.isEqualTo("miss");
assertThat(missTimerValue.count)
.as("Cache gets miss timer count for region " + replicateRegion.getName())
.isEqualTo(5);
assertThat(missTimerValue.totalTime)
.as("Cache gets miss timer total time for region " + replicateRegion.getName())
.isGreaterThan(0);
}
@Test
public void hitTimerRecordsCountAndTotalTime_ifGetsPerformedOnPartitionRegionWithExistingKey()
throws IOException {
givenClusterWithTimeStatisticsEnabled();
for (int i = 0; i < 5; i++) {
partitionRegion.put(i, i);
partitionRegion.get(i);
}
TimerValue hitTimerValue = hitTimerValueForRegion(partitionRegion);
assertThat(hitTimerValue.region)
.as("Cache gets hit timer region tag value")
.isEqualTo(partitionRegion.getName());
assertThat(hitTimerValue.result)
.as("Cache gets hit timer result tag value")
.isEqualTo("hit");
assertThat(hitTimerValue.count)
.as("Cache gets hit timer count for region " + partitionRegion.getName())
.isEqualTo(5);
assertThat(hitTimerValue.totalTime)
.as("Cache gets hit timer total time for region " + partitionRegion.getName())
.isGreaterThan(0);
}
@Test
public void missTimerRecordsCountAndTotalTime_ifGetsPerformedOnPartitionRegionWithMissingKey()
throws IOException {
givenClusterWithTimeStatisticsEnabled();
for (int i = 0; i < 5; i++) {
partitionRegion.get(i);
}
TimerValue missTimerValue = missTimerValueForRegion(partitionRegion);
assertThat(missTimerValue.region)
.as("Cache gets miss timer region tag value")
.isEqualTo(partitionRegion.getName());
assertThat(missTimerValue.result)
.as("Cache gets miss timer result tag value")
.isEqualTo("miss");
assertThat(missTimerValue.count)
.as("Cache gets miss timer count for region " + partitionRegion.getName())
.isEqualTo(5);
assertThat(missTimerValue.totalTime)
.as("Cache gets miss timer total time for region " + partitionRegion.getName())
.isGreaterThan(0);
}
@Test
public void timersExistWithInitialValues_ifNoGetsPerformedOnReplicateRegion() throws IOException {
givenClusterWithTimeStatisticsEnabled();
assertThat(allTimerValuesForRegion(replicateRegion))
.as("All timer values for region " + replicateRegion.getName())
.hasSize(2)
.anyMatch(tv -> tv.result.equals("hit"))
.anyMatch(tv -> tv.result.equals("miss"))
.allMatch(tv -> tv.count == 0, "All timers have count of zero")
.allMatch(tv -> tv.totalTime == 0, "All timers have total time of zero");
}
@Test
public void allTimersRemoved_ifReplicateRegionDestroyed() throws IOException {
givenClusterWithTimeStatisticsEnabled();
assertThat(allTimerValuesForRegion(replicateRegion))
.as("All timer values before destroying region " + replicateRegion.getName())
.hasSize(2);
replicateRegion.destroyRegion();
assertThat(allTimerValuesForRegion(replicateRegion))
.as("All timer values after destroying region " + replicateRegion.getName())
.isEmpty();
}
@Test
public void timersExistWithInitialValues_ifNoGetsPerformedOnPartitionRegion() throws IOException {
givenClusterWithTimeStatisticsEnabled();
assertThat(allTimerValuesForRegion(partitionRegion))
.as("All timer values for region " + partitionRegion.getName())
.hasSize(2)
.anyMatch(tv -> tv.result.equals("hit"))
.anyMatch(tv -> tv.result.equals("miss"))
.allMatch(tv -> tv.count == 0, "All timers have count of zero")
.allMatch(tv -> tv.totalTime == 0, "All timers have total time of zero");
}
@Test
public void allTimersRemoved_ifPartitionRegionDestroyed() throws IOException {
givenClusterWithTimeStatisticsEnabled();
assertThat(allTimerValuesForRegion(partitionRegion))
.as("All timer values before destroying region " + partitionRegion.getName())
.hasSize(2);
partitionRegion.destroyRegion();
assertThat(allTimerValuesForRegion(partitionRegion))
.as("All timer values after destroying region " + partitionRegion.getName())
.isEmpty();
}
@Test
public void timersRecordCountForReplicateRegion_ifTimeStatisticsDisabled() throws IOException {
givenClusterWithTimeStatisticsDisabled();
replicateRegion.put("existing-key", "existing-value");
replicateRegion.get("existing-key");
replicateRegion.get("missing-key");
assertThat(allTimerValuesForRegion(replicateRegion))
.as("All timer values for region " + replicateRegion.getName())
.hasSize(2)
.anyMatch(tv -> tv.result.equals("hit"))
.anyMatch(tv -> tv.result.equals("miss"))
.allMatch(tv -> tv.count == 1, "All timers have count of one")
.allMatch(tv -> tv.totalTime == 0, "All timers have total time of zero");
}
@Test
public void timersRecordCountForPartitionRegion_ifTimeStatisticsDisabled() throws IOException {
givenClusterWithTimeStatisticsDisabled();
partitionRegion.put("existing-key", "existing-value");
partitionRegion.get("existing-key");
partitionRegion.get("missing-key");
assertThat(allTimerValuesForRegion(partitionRegion))
.as("All timer values for region " + partitionRegion.getName())
.hasSize(2)
.anyMatch(tv -> tv.result.equals("hit"))
.anyMatch(tv -> tv.result.equals("miss"))
.allMatch(tv -> tv.count == 1, "All timers have count of one")
.allMatch(tv -> tv.totalTime == 0, "All timers have total time of zero");
}
@Test
public void timersDoNotRecord_ifGetThrows() throws IOException {
givenClusterWithSecurityThatDeniesAllGetOperations();
Throwable thrown = catchThrowable(() -> replicateRegion.get("key"));
assertThat(thrown)
.as("Exception from get operation")
.hasCauseInstanceOf(NotAuthorizedException.class);
assertThat(allTimerValuesForRegion(replicateRegion))
.as("All timer values for region " + replicateRegion.getName())
.hasSize(2)
.allMatch(tv -> tv.count == 0, "All timers have count of zero");
}
private void givenClusterWithTimeStatisticsEnabled() throws IOException {
startCluster(true, false);
}
private void givenClusterWithTimeStatisticsDisabled() throws IOException {
startCluster(false, false);
}
private void givenClusterWithSecurityThatDeniesAllGetOperations() throws IOException {
startCluster(true, true);
}
private void startCluster(boolean enableTimeStatistics, boolean enableSecurity)
throws IOException {
int[] availablePorts = getRandomAvailableTCPPorts(3);
locatorPort = availablePorts[0];
int locatorJmxPort = availablePorts[1];
int serverPort = availablePorts[2];
Path serviceJarPath = serviceJarRule.createJarFor("metrics-publishing-service.jar",
MetricsPublishingService.class, SimpleMetricsPublishingService.class);
Path helpersJarPath = temporaryFolder.getRoot().toPath()
.resolve("helpers.jar").toAbsolutePath();
writeJarFromClasses(helpersJarPath.toFile(), TimerValue.class,
FetchCacheGetsTimerValues.class, DenyAllDataRead.class, ClientSecurityConfig.class);
String startLocatorCommand = String.join(" ",
"start locator",
"--name=" + "locator",
"--dir=" + temporaryFolder.newFolder("locator").getAbsolutePath(),
"--port=" + locatorPort,
"--http-service-port=0",
"--J=-Dgemfire.jmx-manager-port=" + locatorJmxPort,
"--classpath=" + helpersJarPath);
String serverName = "server";
String startServerCommand = String.join(" ",
"start server",
"--name=" + serverName,
"--dir=" + temporaryFolder.newFolder(serverName).getAbsolutePath(),
"--server-port=" + serverPort,
"--locators=localhost[" + locatorPort + "]",
"--classpath=" + serviceJarPath + pathSeparatorChar + helpersJarPath);
if (enableTimeStatistics) {
startServerCommand += " --enable-time-statistics";
}
if (enableSecurity) {
File securityPropertiesFile = createSecurityPropertiesFile();
String securityPropertiesFileOption =
" --security-properties-file=" + securityPropertiesFile.getAbsolutePath();
startLocatorCommand += securityPropertiesFileOption;
startServerCommand += securityPropertiesFileOption;
}
String replicateRegionName = "ReplicateRegion";
String createReplicateRegionCommand = String.join(" ",
"create region",
"--type=REPLICATE",
"--name=" + replicateRegionName);
String partitionRegionName = "PartitionRegion";
String createPartitionRegionCommand = String.join(" ",
"create region",
"--type=PARTITION",
"--name=" + partitionRegionName);
gfshRule.execute(startLocatorCommand, startServerCommand, createReplicateRegionCommand,
createPartitionRegionCommand);
ClientCacheFactory clientCacheFactory = new ClientCacheFactory();
if (enableSecurity) {
clientCacheFactory.set("security-client-auth-init", ClientSecurityConfig.class.getName());
}
clientCache = clientCacheFactory
.addPoolLocator("localhost", locatorPort)
.create();
replicateRegion = clientCache
.createClientRegionFactory(ClientRegionShortcut.PROXY)
.create(replicateRegionName);
partitionRegion = clientCache
.createClientRegionFactory(ClientRegionShortcut.PROXY)
.create(partitionRegionName);
}
private File createSecurityPropertiesFile() throws IOException {
Properties securityProperties = ClientSecurityConfig.securityProperties();
File securityPropertiesFile = gfshRule.getTemporaryFolder().newFile("security.properties");
securityProperties.store(new FileOutputStream(securityPropertiesFile), null);
return securityPropertiesFile;
}
private TimerValue hitTimerValueForRegion(Region<?, ?> region) {
return timerValueForRegionAndResult(region, "hit");
}
private TimerValue missTimerValueForRegion(Region<?, ?> region) {
return timerValueForRegionAndResult(region, "miss");
}
private TimerValue timerValueForRegionAndResult(Region<?, ?> region, String resultTagValue) {
List<TimerValue> cacheGetsTimerValues = allTimerValuesForRegion(region).stream()
.filter(tv -> tv.result.equals(resultTagValue))
.collect(toList());
assertThat(cacheGetsTimerValues)
.as("Timers for region " + region.getName() + " with result " + resultTagValue)
.hasSize(1);
return cacheGetsTimerValues.get(0);
}
private List<TimerValue> allTimerValuesForRegion(Region<?, ?> region) {
@SuppressWarnings("unchecked")
List<List<TimerValue>> timerValuesFromAllServers =
(List<List<TimerValue>>) onServer(clientCache)
.execute(new FetchCacheGetsTimerValues())
.getResult();
assertThat(timerValuesFromAllServers)
.hasSize(1);
return timerValuesFromAllServers.get(0).stream()
.filter(tv -> tv.region.equals(region.getName()))
.collect(toList());
}
static class TimerValue implements Serializable {
final long count;
final double totalTime;
final String region;
final String result;
TimerValue(long count, double totalTime, String region, String result) {
this.count = count;
this.totalTime = totalTime;
this.region = region;
this.result = result;
}
@Override
public String toString() {
return "TimerValue{" +
"count=" + count +
", totalTime=" + totalTime +
", region='" + region + '\'' +
", result='" + result + '\'' +
'}';
}
}
static class FetchCacheGetsTimerValues implements Function<Void> {
private static final String ID = "FetchCacheGetsTimerValues";
@Override
public void execute(FunctionContext<Void> context) {
Collection<Timer> timers = SimpleMetricsPublishingService.getRegistry()
.find("geode.cache.gets")
.timers();
List<TimerValue> result = timers.stream()
.map(FetchCacheGetsTimerValues::toTimerValues)
.collect(toList());
context.getResultSender().lastResult(result);
}
@Override
public String getId() {
return ID;
}
private static TimerValue toTimerValues(Timer t) {
String region = t.getId().getTag("region");
String result = t.getId().getTag("result");
return new TimerValue(
t.count(),
t.totalTime(NANOSECONDS),
region,
result);
}
}
public static class DenyAllDataRead implements org.apache.geode.security.SecurityManager {
@Override
public Object authenticate(Properties credentials) throws AuthenticationFailedException {
String userName = credentials.getProperty(USER_NAME);
if (userName == null) {
throw new AuthenticationFailedException("No user name provided");
}
return userName;
}
@Override
public boolean authorize(Object principal, ResourcePermission permission) {
return !isDataRead(permission);
}
private static boolean isDataRead(ResourcePermission permission) {
return ResourcePermission.Resource.DATA.equals(permission.getResource())
&& ResourcePermission.Operation.READ.equals(permission.getOperation());
}
}
public static class ClientSecurityConfig implements AuthInitialize {
@Override
public Properties getCredentials(Properties securityProps, DistributedMember server,
boolean isPeer) throws AuthenticationFailedException {
return securityProperties();
}
static Properties securityProperties() {
Properties securityProperties = new Properties();
securityProperties.setProperty(SECURITY_MANAGER, DenyAllDataRead.class.getName());
securityProperties.setProperty("security-username", "user");
securityProperties.setProperty("security-password", "pass");
return securityProperties;
}
}
}