mirror of
https://github.com/google/nomulus
synced 2026-01-07 14:05:44 +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):
|
{"node_modules/"}, REQUIRED):
|
||||||
"Source files must end in a newline.",
|
"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(
|
PresubmitCheck(
|
||||||
r".*\bSystem\.(out|err)\.print", "java", {
|
r".*\bSystem\.(out|err)\.print", "java", {
|
||||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||||
"RegistryTestServerMain.java", "TestServerExtension.java",
|
"/load-testing/", "RegistryTestServerMain.java",
|
||||||
"FlowDocumentationTool.java"
|
"TestServerExtension.java", "FlowDocumentationTool.java"
|
||||||
}):
|
}):
|
||||||
"System.(out|err).println is only allowed in tools/ packages. Please "
|
"System.(out|err).println is only allowed in tools/ packages. Please "
|
||||||
"use a logger instead.",
|
"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 'services:pubapi'
|
||||||
include 'console-webapp'
|
include 'console-webapp'
|
||||||
include 'jetty'
|
include 'jetty'
|
||||||
|
include 'load-testing'
|
||||||
|
|||||||
Reference in New Issue
Block a user