mirror of
https://github.com/google/nomulus
synced 2025-12-22 22:07:11 +00:00
Create a load testing EPP client (#2415)
* Create a load testing EPP client. This code is mostly based off of what was used for a past EPP load testing client that can be found in Google3 at https://source.corp.google.com/piper///depot/google3/experimental/users/jianglai/proxy/java/google/registry/proxy/client/ I modified the old client to be open-source friendly and use Gradle. For now, this only performs a login and logout command, I will further expand on this in later PRs to add other EPP commands so that we can truly load test the system. * Small changes * Remove unnecessary build dep * Add gradle build tasks * Small fixes * Add an instances setUp and cleanUp script * More modifications to instance setup scripts * change to ubuntu instance * Add comment to make ssh work
This commit is contained in:
@@ -100,12 +100,12 @@ PRESUBMITS = {
|
||||
{"node_modules/"}, REQUIRED):
|
||||
"Source files must end in a newline.",
|
||||
|
||||
# System.(out|err).println should only appear in tools/
|
||||
# System.(out|err).println should only appear in tools/ or load-testing/
|
||||
PresubmitCheck(
|
||||
r".*\bSystem\.(out|err)\.print", "java", {
|
||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||
"RegistryTestServerMain.java", "TestServerExtension.java",
|
||||
"FlowDocumentationTool.java"
|
||||
"/load-testing/", "RegistryTestServerMain.java",
|
||||
"TestServerExtension.java", "FlowDocumentationTool.java"
|
||||
}):
|
||||
"System.(out|err).println is only allowed in tools/ packages. Please "
|
||||
"use a logger instead.",
|
||||
|
||||
54
load-testing/build.gradle
Normal file
54
load-testing/build.gradle
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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.
|
||||
|
||||
apply plugin: 'java'
|
||||
|
||||
createUberJar('buildLoadTestClient', 'loadTest', 'google.registry.client.EppClient')
|
||||
|
||||
dependencies {
|
||||
def deps = rootProject.dependencyMap
|
||||
implementation deps['joda-time:joda-time']
|
||||
implementation deps['io.netty:netty-buffer']
|
||||
implementation deps['io.netty:netty-codec']
|
||||
implementation deps['io.netty:netty-codec-http']
|
||||
implementation deps['io.netty:netty-common']
|
||||
implementation deps['io.netty:netty-handler']
|
||||
implementation deps['io.netty:netty-transport']
|
||||
implementation deps['com.google.guava:guava']
|
||||
implementation deps['org.bouncycastle:bcpg-jdk18on']
|
||||
implementation deps['org.bouncycastle:bcpkix-jdk18on']
|
||||
implementation deps['org.bouncycastle:bcprov-jdk18on']
|
||||
implementation deps['org.jcommander:jcommander']
|
||||
implementation deps['com.google.flogger:flogger']
|
||||
runtimeOnly deps['com.google.flogger:flogger-system-backend']
|
||||
}
|
||||
|
||||
task makeStagingDirectory {
|
||||
mkdir layout.buildDirectory.dir('stage')
|
||||
}
|
||||
|
||||
task copyFilesToStaging(dependsOn: makeStagingDirectory, type: Copy) {
|
||||
from layout.buildDirectory.file('libs/loadTest.jar'), "${projectDir}/certificate.pem", "${projectDir}/key.pem"
|
||||
into layout.buildDirectory.dir('stage')
|
||||
}
|
||||
|
||||
task deployLoadTestsToInstances (dependsOn: copyFilesToStaging, type: Exec) {
|
||||
executable "sh"
|
||||
workingDir "${projectDir}/"
|
||||
args "-c", "./deploy.sh"
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
17
load-testing/deploy.sh
Executable file
17
load-testing/deploy.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed 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.
|
||||
|
||||
HOSTS=$(gcloud compute instances list | awk '/^loadtest/ { print $5 }')
|
||||
for host in $HOSTS; do rsync -avz ./build/stage/ $host:test-client/; done
|
||||
19
load-testing/instanceCleanUp.sh
Executable file
19
load-testing/instanceCleanUp.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed 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.
|
||||
|
||||
# Find and delete the instances used for load testing
|
||||
|
||||
gcloud compute instances list --filter="name ~ loadtest.*" --zones us-east4-a \
|
||||
--format="value(name)" | xargs gcloud compute instances delete --zone us-east4-a
|
||||
39
load-testing/instanceSetUp.sh
Executable file
39
load-testing/instanceSetUp.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed 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.
|
||||
|
||||
# Create the instances - modify this number for the amount of instances you
|
||||
# would like to use for your load test
|
||||
gcloud compute instances create loadtest-{1..2} --machine-type g1-small \
|
||||
--image-family ubuntu-2204-lts --image-project ubuntu-os-cloud --zone us-east4-a
|
||||
|
||||
sleep 10
|
||||
|
||||
# Get all the created load tests instances
|
||||
HOSTS=$(gcloud compute instances list | awk '/^loadtest/ { print $5 }')
|
||||
|
||||
#Install rsync and Java - Retry is needed here since ssh connection will fail until instances are fully provisioned
|
||||
for host in $HOSTS;
|
||||
do
|
||||
for i in {1..60}; do
|
||||
if ssh $host 'sudo apt-get -y update &&
|
||||
sudo apt-get -y upgrade &&
|
||||
sudo apt-get -y install rsync &&
|
||||
sudo apt-get -y install openjdk-21-jdk'; then
|
||||
break
|
||||
else
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
done
|
||||
21
load-testing/run.sh
Normal file
21
load-testing/run.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed 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.
|
||||
|
||||
HOSTS=$(gcloud compute instances list | awk '/^loadtest/ { print $5 }')
|
||||
|
||||
for host in $HOSTS;
|
||||
do ssh $host 'cd test-client/ &&
|
||||
java -jar loadTest.jar --host epp.example --certificate certificate.pem -k key.pem -pw examplePassword -ft';
|
||||
done
|
||||
353
load-testing/src/main/java/google/registry/client/EppClient.java
Normal file
353
load-testing/src/main/java/google/registry/client/EppClient.java
Normal file
@@ -0,0 +1,353 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 google.registry.client;
|
||||
|
||||
import static com.google.common.io.Resources.getResource;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import com.beust.jcommander.IStringConverter;
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.io.Resources;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
|
||||
import io.netty.handler.codec.LengthFieldPrepender;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyPair;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** A simple EPP client that can be used for load testing. */
|
||||
@Parameters(separators = " =")
|
||||
@SuppressWarnings("FutureReturnValueIgnored")
|
||||
public class EppClient implements Runnable {
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
private static final String LOGIN_FILE = "login.xml";
|
||||
private static final String LOGOUT_FILE = "logout.xml";
|
||||
static final AttributeKey<ArrayList<ZonedDateTime>> REQUEST_SENT =
|
||||
AttributeKey.valueOf("REQUEST_SENT");
|
||||
static final AttributeKey<ArrayList<ZonedDateTime>> RESPONSE_RECEIVED =
|
||||
AttributeKey.valueOf("RESPONSE_RECEIVED");
|
||||
static final AttributeKey<Integer> CHANNEL_NUMBER = AttributeKey.valueOf("CHANNEL_NUMBER");
|
||||
static final AttributeKey<Path> LOGGING_LOCATION = AttributeKey.valueOf("LOGGING_LOCATION");
|
||||
static final AttributeKey<Boolean> FORCE_TERMINATE = AttributeKey.valueOf("FORCE_TERMINATE");
|
||||
static final AttributeKey<ExecutorService> LOGGING_EXECUTOR =
|
||||
AttributeKey.valueOf("LOGGING_EXECUTOR");
|
||||
static final AttributeKey<Iterator<byte[]>> INPUT_ITERATOR =
|
||||
AttributeKey.valueOf("INPUT_ITERATOR");
|
||||
static final AttributeKey<Promise<Void>> LOGGING_REQUEST_COMPLETE =
|
||||
AttributeKey.valueOf("LOGGING_REQUEST_COMPLETE");
|
||||
private static final int PORT = 700;
|
||||
private static final int TIMEOUT_SECONDS = 600;
|
||||
|
||||
public static class InetAddressConverter implements IStringConverter<InetAddress> {
|
||||
|
||||
@Override
|
||||
public InetAddress convert(String host) {
|
||||
try {
|
||||
return InetAddress.getByName(host);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException(host, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parameter(
|
||||
names = {"--help"},
|
||||
description = "Print this help message.",
|
||||
help = true)
|
||||
private boolean help = false;
|
||||
|
||||
@Parameter(
|
||||
names = {"--host", "-h"},
|
||||
description = "Epp server hostname/IP to connect to.",
|
||||
converter = InetAddressConverter.class,
|
||||
required = true)
|
||||
private InetAddress host = null;
|
||||
|
||||
@Parameter(
|
||||
names = {"--certificate"},
|
||||
description = "Certificate pem file.",
|
||||
required = true)
|
||||
private String certFileName = null;
|
||||
|
||||
@Parameter(
|
||||
names = {"--key", "-k"},
|
||||
description = "Private key pem file.",
|
||||
required = true)
|
||||
private String keyFileName = null;
|
||||
|
||||
@Parameter(
|
||||
names = {"--connections", "-cn"},
|
||||
description = "Number of connections that are made to the EPP server.")
|
||||
private int connections = 1;
|
||||
|
||||
@Parameter(
|
||||
names = {"--client", "-c"},
|
||||
description = "Registrar client id.")
|
||||
private String client = "proxy";
|
||||
|
||||
@Parameter(
|
||||
names = {"--password", "-pw"},
|
||||
description = "Registrar password.")
|
||||
private String password = "abcde12345";
|
||||
|
||||
@Parameter(
|
||||
names = {"--force_terminate", "-ft"},
|
||||
description = "Whether to explicitly close the connection after receiving a logout response.")
|
||||
private boolean forceTerminate = false;
|
||||
|
||||
public static void main(String[] args) {
|
||||
EppClient eppClient = new EppClient();
|
||||
JCommander jCommander = new JCommander(eppClient);
|
||||
jCommander.parse(args);
|
||||
if (eppClient.help) {
|
||||
jCommander.usage();
|
||||
return;
|
||||
}
|
||||
eppClient.run();
|
||||
}
|
||||
|
||||
private ImmutableList<String> makeInputList(ZonedDateTime now) {
|
||||
ImmutableList.Builder<String> templatesList = ImmutableList.builder();
|
||||
ImmutableList.Builder<String> inputList = ImmutableList.builder();
|
||||
templatesList.add(readStringFromFile(LOGIN_FILE));
|
||||
templatesList.add(readStringFromFile(LOGOUT_FILE));
|
||||
for (String template : templatesList.build()) {
|
||||
inputList.add(
|
||||
template
|
||||
.replace("@@CLIENT@@", client)
|
||||
.replace("@@PASSWORD@@", password)
|
||||
.replace("@@NOW@@", now.toString()));
|
||||
}
|
||||
return inputList.build();
|
||||
}
|
||||
|
||||
private static String readStringFromFile(String filename) {
|
||||
try {
|
||||
return Resources.toString(getResource(EppClient.class, "resources/" + filename), UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Cannot read from file: resources/" + filename);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyPair getKeyPair(String filename) throws IOException {
|
||||
byte[] keyBytes = Files.asCharSource(new File(filename), UTF_8).read().getBytes(UTF_8);
|
||||
try {
|
||||
PEMKeyPair pemPair =
|
||||
(PEMKeyPair)
|
||||
new PEMParser(new InputStreamReader(new ByteArrayInputStream(keyBytes), UTF_8))
|
||||
.readObject();
|
||||
return new JcaPEMKeyConverter().setProvider("BC").getKeyPair(pemPair);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate getCertificate(String filename) throws IOException {
|
||||
byte[] certificateBytes = Files.asCharSource(new File(filename), UTF_8).read().getBytes(UTF_8);
|
||||
try {
|
||||
X509CertificateHolder certificateHolder =
|
||||
(X509CertificateHolder)
|
||||
new PEMParser(
|
||||
new InputStreamReader(new ByteArrayInputStream(certificateBytes), UTF_8))
|
||||
.readObject();
|
||||
return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certificateHolder);
|
||||
} catch (IOException | CertificateException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelInitializer<SocketChannel> makeChannelInitializer(
|
||||
String outputFolder, ImmutableList<ExecutorService> loggingExecutors) throws IOException {
|
||||
return new ChannelInitializer<>() {
|
||||
|
||||
private final ImmutableList<String> inputList =
|
||||
makeInputList(ZonedDateTime.now(ZoneOffset.UTC));
|
||||
private final KeyPair key = getKeyPair(keyFileName);
|
||||
private final X509Certificate cert = getCertificate(certFileName);
|
||||
private final LoggingHandler loggingHandler = new LoggingHandler(LogLevel.INFO);
|
||||
private final EppClientHandler eppClientHandler = new EppClientHandler();
|
||||
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) throws Exception {
|
||||
ch.attr(REQUEST_SENT).set(new ArrayList<>());
|
||||
ch.attr(RESPONSE_RECEIVED).set(new ArrayList<>());
|
||||
Path loggingLocation =
|
||||
Paths.get(String.format("%s/%d.log", outputFolder, ch.attr(CHANNEL_NUMBER).get()));
|
||||
ch.attr(LOGGING_LOCATION).set(loggingLocation);
|
||||
ch.attr(FORCE_TERMINATE).set(forceTerminate);
|
||||
ch.attr(LOGGING_EXECUTOR)
|
||||
.set(loggingExecutors.get(ch.attr(CHANNEL_NUMBER).get() % loggingExecutors.size()));
|
||||
|
||||
ch.attr(INPUT_ITERATOR)
|
||||
.set(
|
||||
inputList.stream()
|
||||
.map(
|
||||
(String str) ->
|
||||
str.replace(
|
||||
"@@CHANNEL_NUMBER@@",
|
||||
String.valueOf(ch.attr(CHANNEL_NUMBER).get()))
|
||||
.replace("@@CHANNEL_NUMBER_MODULO@@", String.valueOf(0))
|
||||
.getBytes(UTF_8))
|
||||
.iterator());
|
||||
|
||||
ch.pipeline()
|
||||
.addLast(
|
||||
SslContextBuilder.forClient()
|
||||
.keyManager(key.getPrivate(), cert)
|
||||
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
||||
.build()
|
||||
.newHandler(ch.alloc(), host.getHostName(), PORT));
|
||||
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(512 * 1024, 0, 4, -4, 4));
|
||||
ch.pipeline().addLast(new LengthFieldPrepender(4, true));
|
||||
ch.pipeline().addLast(loggingHandler);
|
||||
ch.pipeline().addLast(eppClientHandler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String createOutputFolder(String folderName) {
|
||||
Path folder = Paths.get(folderName);
|
||||
if (!folder.toFile().exists()) {
|
||||
folder.toFile().mkdirs();
|
||||
}
|
||||
System.out.printf("\nOutputs saved at %s\n", folder);
|
||||
return folderName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
String outputFolder = createOutputFolder(String.format("load-tests/%s", DateTime.now(UTC)));
|
||||
ImmutableList.Builder<ExecutorService> builder = ImmutableList.builderWithExpectedSize(5);
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
builder.add(Executors.newSingleThreadExecutor());
|
||||
}
|
||||
final ImmutableList<ExecutorService> loggingExecutors = builder.build();
|
||||
|
||||
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
|
||||
|
||||
try {
|
||||
Bootstrap bootstrap =
|
||||
new Bootstrap()
|
||||
.group(eventLoopGroup)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(makeChannelInitializer(outputFolder, loggingExecutors));
|
||||
|
||||
List<ChannelFuture> channelFutures = new ArrayList<>();
|
||||
|
||||
// Three requests: hello (from the proxy), login and logout.
|
||||
int requestPerConnection = 3;
|
||||
|
||||
for (int i = 0; i < connections; i++) {
|
||||
bootstrap.attr(CHANNEL_NUMBER, i);
|
||||
channelFutures.add(
|
||||
bootstrap
|
||||
.connect(host, PORT)
|
||||
.addListener(
|
||||
(ChannelFuture cf) -> {
|
||||
if (!cf.isSuccess()) {
|
||||
System.out.printf("Cannot connect to %s:%s\n", host, PORT);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
LinkedHashSet<Integer> killedConnections = new LinkedHashSet<>();
|
||||
|
||||
// Wait for all channels to close.
|
||||
for (ChannelFuture channelFuture : channelFutures) {
|
||||
Channel channel = channelFuture.syncUninterruptibly().channel();
|
||||
int channelNumber = channel.attr(CHANNEL_NUMBER).get();
|
||||
if (!channel
|
||||
.closeFuture()
|
||||
.awaitUninterruptibly(
|
||||
TIMEOUT_SECONDS * 1000
|
||||
- Duration.between(
|
||||
channel.attr(REQUEST_SENT).get().getFirst(),
|
||||
ZonedDateTime.now(ZoneOffset.UTC))
|
||||
.toMillis())) {
|
||||
channel.close().syncUninterruptibly();
|
||||
killedConnections.add(channelNumber);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("====== SUMMARY ======");
|
||||
System.out.printf("Number of connections: %d\n", connections);
|
||||
System.out.printf("Number of requests per connection: %d\n", requestPerConnection);
|
||||
if (!killedConnections.isEmpty()) {
|
||||
System.out.printf("Force killed connections (%d): ", killedConnections.size());
|
||||
for (int channelNumber : killedConnections) {
|
||||
System.out.printf("%d ", channelNumber);
|
||||
}
|
||||
System.out.print("\n");
|
||||
}
|
||||
|
||||
eventLoopGroup.shutdownGracefully();
|
||||
|
||||
channelFutures.forEach(
|
||||
channelFuture -> {
|
||||
channelFuture.channel().attr(LOGGING_REQUEST_COMPLETE).get().syncUninterruptibly();
|
||||
});
|
||||
loggingExecutors.forEach(ExecutorService::shutdown);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed 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 google.registry.client;
|
||||
|
||||
import static google.registry.client.EppClient.CHANNEL_NUMBER;
|
||||
import static google.registry.client.EppClient.FORCE_TERMINATE;
|
||||
import static google.registry.client.EppClient.INPUT_ITERATOR;
|
||||
import static google.registry.client.EppClient.LOGGING_EXECUTOR;
|
||||
import static google.registry.client.EppClient.LOGGING_LOCATION;
|
||||
import static google.registry.client.EppClient.LOGGING_REQUEST_COMPLETE;
|
||||
import static google.registry.client.EppClient.REQUEST_SENT;
|
||||
import static google.registry.client.EppClient.RESPONSE_RECEIVED;
|
||||
import static java.nio.file.StandardOpenOption.APPEND;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/** Handler that sends EPP requests and receives EPP responses. */
|
||||
@SuppressWarnings("FutureReturnValueIgnored")
|
||||
@Sharable
|
||||
public class EppClientHandler extends ChannelDuplexHandler {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
static class FileWriter implements Runnable {
|
||||
|
||||
private final Path loggingLocation;
|
||||
private final byte[] contents;
|
||||
private final ZonedDateTime time;
|
||||
|
||||
FileWriter(Path loggingLocation, byte[] contents, ZonedDateTime time) {
|
||||
this.loggingLocation = loggingLocation;
|
||||
this.contents = contents;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!Files.exists(loggingLocation)) {
|
||||
Files.createFile(loggingLocation);
|
||||
}
|
||||
Files.writeString(loggingLocation, time.toString() + "\n", APPEND);
|
||||
Files.write(loggingLocation, contents, APPEND);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
ctx.channel().attr(REQUEST_SENT).get().add(now);
|
||||
ctx.channel().attr(LOGGING_REQUEST_COMPLETE).set(ctx.executor().newPromise());
|
||||
super.channelRegistered(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
Promise<Void> loggingCompletePromise = ctx.channel().attr(LOGGING_REQUEST_COMPLETE).get();
|
||||
if (!loggingCompletePromise.isDone()) {
|
||||
loggingCompletePromise.setSuccess(null);
|
||||
}
|
||||
logger.atWarning().withCause(cause).log(
|
||||
"Connection %d closed.", ctx.channel().attr(CHANNEL_NUMBER).get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
Channel ch = ctx.channel();
|
||||
ctx.channel().attr(RESPONSE_RECEIVED).get().add(now);
|
||||
if (msg instanceof ByteBuf buffer) {
|
||||
byte[] contents = new byte[buffer.readableBytes()];
|
||||
buffer.readBytes(contents);
|
||||
ReferenceCountUtil.release(buffer);
|
||||
if (ch.attr(LOGGING_LOCATION).get() != null) {
|
||||
ch.attr(LOGGING_EXECUTOR)
|
||||
.get()
|
||||
.submit(new FileWriter(ch.attr(LOGGING_LOCATION).get(), contents, now));
|
||||
}
|
||||
if (ch.attr(INPUT_ITERATOR).get().hasNext()) {
|
||||
ch.writeAndFlush(ch.attr(INPUT_ITERATOR).get().next());
|
||||
} else {
|
||||
ch.attr(LOGGING_REQUEST_COMPLETE).get().setSuccess(null);
|
||||
if (ch.attr(FORCE_TERMINATE).get()) {
|
||||
ch.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
Channel ch = ctx.channel();
|
||||
ctx.channel().attr(REQUEST_SENT).get().add(now);
|
||||
if (msg instanceof byte[] outputBytes) {
|
||||
ByteBuf buffer = Unpooled.buffer();
|
||||
buffer.writeBytes(outputBytes);
|
||||
ctx.write(buffer, promise);
|
||||
if (ch.attr(LOGGING_LOCATION).get() != null) {
|
||||
ch.attr(LOGGING_EXECUTOR)
|
||||
.get()
|
||||
.submit(new FileWriter(ch.attr(LOGGING_LOCATION).get(), outputBytes, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
|
||||
Promise<Void> loggingCompletePromise = ctx.channel().attr(LOGGING_REQUEST_COMPLETE).get();
|
||||
if (!loggingCompletePromise.isDone()) {
|
||||
loggingCompletePromise.setSuccess(null);
|
||||
}
|
||||
super.close(ctx, promise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
Promise<Void> loggingCompletePromise = ctx.channel().attr(LOGGING_REQUEST_COMPLETE).get();
|
||||
if (!loggingCompletePromise.isDone()) {
|
||||
loggingCompletePromise.setSuccess(null);
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
73
load-testing/src/main/java/google/registry/client/README.md
Normal file
73
load-testing/src/main/java/google/registry/client/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
## EPP Load Testing Client
|
||||
|
||||
This project contains an EPP client that can be use for load testing the full
|
||||
registry platform. All the below commands should be run from the merged root.
|
||||
|
||||
### Setting up the test instances
|
||||
|
||||
* If you have not done s yet, you will need to set up ssh keys in your
|
||||
[GCE metadata](https://pantheon.corp.google.com/compute/metadata?resourceTab=sshkeys):
|
||||
|
||||
* To create however many GCE instances you want to run on, modify the
|
||||
`instanceSetUp.sh` file to include the correct number of instances.
|
||||
|
||||
* Run the instance set up script to create and configure each of the GCE
|
||||
instances to be used for load testing:
|
||||
|
||||
```shell
|
||||
$ load-testing/instanceSetUp.sh
|
||||
```
|
||||
|
||||
* Verify that the IP address of any created instances is in the allowlist of the
|
||||
proxy registrar user.
|
||||
* Use the below command to get the IP addresses of the created instances.
|
||||
|
||||
```shell
|
||||
$ (gcloud compute instances list | awk '/^loadtest/ { print $5 }')
|
||||
```
|
||||
|
||||
* Check the proxy registrar's current allow list
|
||||
|
||||
```shell
|
||||
$ nomulus -e sandbox get_registrar proxy | grep ipAddressAllowList
|
||||
```
|
||||
|
||||
* All of your host ip addresses should match a netmask specified in the proxy
|
||||
allow list. If not, you'll need to redefine the list:
|
||||
|
||||
```shell
|
||||
$ nomulus -e sandbox update_registrar proxy --ip_allow_list=<new-comma-separated-allowlist>
|
||||
```
|
||||
|
||||
|
||||
### Running the client
|
||||
|
||||
* From the merged root build the load testing client:
|
||||
```shell
|
||||
$ ./nom_build :load-testing:buildLoadTestClient
|
||||
```
|
||||
|
||||
* Deploy the client to the GCE instances (this will create a local staging
|
||||
directory and deploy it to each of your previously created loadtest GCE instances):
|
||||
```shell
|
||||
$ ./nom_build :load-testing:deployLoadTestsToInstances
|
||||
```
|
||||
|
||||
* Run the load test. Configurations of the load test can be made by configuring
|
||||
this `run.sh` file locally.
|
||||
|
||||
```shell
|
||||
$ load-testing/run.sh
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
* Run the instance clean up script to delete the created instances
|
||||
|
||||
```shell
|
||||
$ load-testing/instanceCleanUp.sh
|
||||
```
|
||||
|
||||
* You may want to remove any host key fingerprints for those hosts from your ~/.ssh/known_hosts file (these IPs tend to get reused with new host keys)
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<login>
|
||||
<clID>@@CLIENT@@</clID>
|
||||
<pw>@@PASSWORD@@</pw>
|
||||
<options>
|
||||
<version>1.0</version>
|
||||
<lang>en</lang>
|
||||
</options>
|
||||
<svcs>
|
||||
<objURI>urn:ietf:params:xml:ns:host-1.0</objURI>
|
||||
<objURI>urn:ietf:params:xml:ns:domain-1.0</objURI>
|
||||
<objURI>urn:ietf:params:xml:ns:contact-1.0</objURI>
|
||||
</svcs>
|
||||
</login>
|
||||
<clTRID>epp-client-login-@@NOW@@-@@CHANNEL_NUMBER@@</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<logout/>
|
||||
<clTRID>epp-client-logout-@@NOW@@-@@CHANNEL_NUMBER@@</clTRID>
|
||||
</command>
|
||||
</epp>
|
||||
@@ -47,3 +47,4 @@ include 'services:tools'
|
||||
include 'services:pubapi'
|
||||
include 'console-webapp'
|
||||
include 'jetty'
|
||||
include 'load-testing'
|
||||
|
||||
Reference in New Issue
Block a user