diff --git a/README.md b/README.md index 34f5ca5..9cc9ba2 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,21 @@ to true in the jmeter.properties file. ## Buffer capacity By default, the size of the downloaded resources is set to 2 MB (2097152 bytes) but, the limit can be increased by adding the `httpJettyClient.maxBufferSize` property on the jmeter.properties file in bytes. + +## Properties +This document describes JMeter properties. The properties present in jmeter.properties also should be set in the user.properties file. These properties are only taken into account after restarting JMeter as they are usually resolved when the class is loaded. + +| **Attribute** | **Description** | **Default** | +|-----------------------------------------------------|----------------------------------------------------------------------------------|-------------| +| **httpJettyClient.maxBufferSize** | Maximum size of the downloaded resources in bytes | 2097152 | +| **httpJettyClient.minThreads** | Minimum number of threads per http client | 1 | +| **httpJettyClient.maxThreads** | Maximum number of threads per http client | 5 | +| **httpJettyClient.maxRequestsQueuedPerDestination** | Maximum number of requests that may be queued to a destination | 32767 | +| **httpJettyClient.maxConnectionsPerDestination** | Sets the max number of connections to open to each destinations | 1 | +| **httpJettyClient.byteBufferPoolFactor** | Factor number used in the allocation of memory in the buffer of http client | 4 | +| **httpJettyClient.strictEventOrdering** | Force request events ordering | false | +| **httpJettyClient.removeIdleDestinations** | Whether destinations that have no connections should be removed | true | +| **httpJettyClient.idleTimeout** | the max time, in milliseconds, a connection can be idle | 30000 | +| **httpJettyClient.auth.preemptive** | Use of Basic preemptive authentication results | false | +| **HTTPSampler.response_timeout** | Maximum waiting time of request without timeout defined, in milliseconds | 0 | +| **http.post_add_content_type_if_missing** | Add to POST a Header Content-type: application/x-www-form-urlencoded if missing? | false | diff --git a/pom.xml b/pom.xml index 74250dd..9f77a9c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.blazemeter.jmeter jmeter-bzm-http2 jar - 2.0.1 + 2.0.2 HTTP/2 Sampler HTTP/2 protocol sampler @@ -15,7 +15,7 @@ UTF-8 UTF-8 5.4.1 - 11.0.6 + 11.0.10 diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java index 693ea7d..4e8b60b 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java +++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java @@ -61,13 +61,16 @@ import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.Fields; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HTTP2JettyClient { - + public static final MappedByteBufferPool BUFFER_POOL = new MappedByteBufferPool( + JMeterUtils.getPropDefault("httpJettyClient.byteBufferPoolFactor", 4)); private static final Logger LOG = LoggerFactory.getLogger(HTTP2JettyClient.class); private static final Set SUPPORTED_METHODS = new HashSet<>(Arrays .asList(HTTPConstants.GET, HTTPConstants.POST, HTTPConstants.PUT, HTTPConstants.PATCH, @@ -80,13 +83,27 @@ public class HTTP2JettyClient { private static final String MULTI_PART_SEPARATOR = "--"; private static final String LINE_SEPARATOR = "\r\n"; private static final String DEFAULT_FILE_MIME_TYPE = "application/octet-stream"; + private int requestTimeout = 0; + private int maxBufferSize = 2 * 1024 * 1024; + private int maxThreads = 5; + private int minThreads = 1; + private int maxRequestsQueuedPerDestination = Short.MAX_VALUE; + private int maxConnectionsPerDestination = 1; + private boolean strictEventOrdering = false; + private boolean removeIdleDestinations = true; + private int idleTimeout = 30000; private final HttpClient httpClient; private boolean http1UpgradeRequired; public HTTP2JettyClient(boolean http1UpgradeRequired) { + loadProperties(); ClientConnector clientConnector = new ClientConnector(); SslContextFactory.Client sslContextFactory = new JMeterJettySslContextFactory(); clientConnector.setSslContextFactory(sslContextFactory); + QueuedThreadPool queuedThreadPool = new QueuedThreadPool(maxThreads); + queuedThreadPool.setMinThreads(minThreads); + queuedThreadPool.setName("HttpClient"); + clientConnector.setExecutor(queuedThreadPool); ClientConnectionFactory.Info http11 = HttpClientConnectionFactory.HTTP11; HTTP2Client http2Client = new HTTP2Client(clientConnector); ClientConnectionFactoryOverHTTP2.HTTP2 http2 = new ClientConnectionFactoryOverHTTP2.HTTP2( @@ -96,6 +113,13 @@ public HTTP2JettyClient(boolean http1UpgradeRequired) { : new ClientConnectionFactory.Info[]{http2, http11}; HttpClientTransport transport = new HttpClientTransportDynamic(clientConnector, protocols); this.httpClient = new HttpClient(transport); + this.httpClient.setUserAgentField(null); // No set UA header + this.httpClient.setByteBufferPool(HTTP2JettyClient.BUFFER_POOL); + this.httpClient.setMaxRequestsQueuedPerDestination(maxRequestsQueuedPerDestination); + this.httpClient.setMaxConnectionsPerDestination(maxConnectionsPerDestination); + this.httpClient.setStrictEventOrdering(strictEventOrdering); + this.httpClient.setRemoveIdleDestinations(removeIdleDestinations); + this.httpClient.setIdleTimeout(idleTimeout); this.http1UpgradeRequired = http1UpgradeRequired; } @@ -103,9 +127,42 @@ public HTTP2JettyClient() { this(false); } + public static void clearBufferPool() { + HTTP2JettyClient.BUFFER_POOL.clear(); + } + + public void loadProperties() { + requestTimeout = JMeterUtils.getPropDefault("HTTPSampler.response_timeout", 0); + maxBufferSize = + Integer.parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxBufferSize", + String.valueOf(2 * 1024 * 1024))); + minThreads = Integer + .parseInt(JMeterUtils.getPropDefault("httpJettyClient.minThreads", + String.valueOf(minThreads))); + maxThreads = Integer + .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxThreads", + String.valueOf(maxThreads))); + maxRequestsQueuedPerDestination = Integer + .parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxRequestsQueuedPerDestination", + String.valueOf(maxRequestsQueuedPerDestination))); + maxConnectionsPerDestination = + Integer.parseInt(JMeterUtils.getPropDefault("httpJettyClient.maxConnectionsPerDestination", + String.valueOf(maxConnectionsPerDestination))); + strictEventOrdering = + Boolean.parseBoolean(JMeterUtils.getPropDefault("httpJettyClient.strictEventOrdering", + String.valueOf(strictEventOrdering))); + removeIdleDestinations = + Boolean.parseBoolean(JMeterUtils.getPropDefault("httpJettyClient.removeIdleDestinations", + String.valueOf(removeIdleDestinations))); + idleTimeout = + Integer.parseInt(JMeterUtils.getPropDefault("httpJettyClient.idleTimeout", + String.valueOf(idleTimeout))); + } + public void start() throws Exception { if (!httpClient.isStarted()) { httpClient.start(); + httpClient.getContentDecoderFactories().clear(); // Clear default headers } } @@ -165,22 +222,24 @@ public HTTPSampleResult sample(HTTP2Sampler sampler, HTTPSampleResult result, public ContentResponse send(HttpRequest request) throws InterruptedException, TimeoutException, ExecutionException { - String maxBufferSizeString = JMeterUtils.getPropDefault("httpJettyClient.maxBufferSize", - String.valueOf(2 * 1024 * 1024)); - if (LOG.isDebugEnabled()) { LOG.debug("Sending request: {}", request); - LOG.debug("Setting max buffer size to {}", maxBufferSizeString); + LOG.debug("Setting max buffer size to {}", maxBufferSize); } FutureResponseListener listener = - new FutureResponseListener(request, Integer.parseInt(maxBufferSizeString)); + new FutureResponseListener(request, maxBufferSize); request.send(listener); - int timeout = JMeterUtils.getPropDefault("HTTPSampler.response_timeout", 2000); - + long getStart = System.currentTimeMillis(); try { - return listener.get(timeout, TimeUnit.MILLISECONDS); + if (requestTimeout > 0) { + int extraTime = 2000; + return listener.get(requestTimeout + extraTime, TimeUnit.MILLISECONDS); + } else { + return listener.get(); + } } catch (TimeoutException e) { - throw new TimeoutException("The request took more than " + timeout + long endGet = System.currentTimeMillis(); + throw new TimeoutException("The request took more than " + (endGet - getStart) + " milliseconds to complete"); } catch (ExecutionException e) { if (e.getCause() != null && e.getCause() instanceof TimeoutException) { @@ -242,6 +301,8 @@ private void setTimeouts(HTTP2Sampler sampler, HttpRequest request) { } if (sampler.getResponseTimeout() > 0) { request.timeout(sampler.getResponseTimeout(), TimeUnit.MILLISECONDS); + } else if (requestTimeout > 0) { + request.timeout(requestTimeout, TimeUnit.MILLISECONDS); } } @@ -542,4 +603,16 @@ private void saveCookiesInCookieManager(ContentResponse response, URL url, } } + public void clearCookies() { + httpClient.getCookieStore().removeAll(); + } + + public void clearAuthenticationResults() { + httpClient.getAuthenticationStore().clearAuthenticationResults(); + } + + public String dump() { + return httpClient.dump(); + } + } diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java index 367eea8..4c92a56 100644 --- a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java +++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java @@ -17,6 +17,7 @@ import org.apache.jmeter.testelement.ThreadListener; import org.apache.jmeter.threads.JMeterContextService; import org.apache.jmeter.threads.JMeterVariables; +import org.apache.jmeter.util.JMeterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +29,8 @@ public class HTTP2Sampler extends HTTPSamplerBase implements LoopIterationListen .withInitial(HashMap::new); private static final String HTTP1_UPGRADE_PROPERTY = "HTTP2Sampler.http1_upgrade"; private final transient Callable clientFactory; + private final boolean dumpAtThreadEnd = JMeterUtils.getPropDefault( + "httpJettyClient.DumpAtThreadEnd", false); public HTTP2Sampler() { setName("HTTP2 Sampler"); @@ -109,7 +112,7 @@ public HTTPSampleResult resultProcessing(final boolean pAreFollowingRedirect, public void iterationStart(LoopIterationEvent iterEvent) { JMeterVariables jMeterVariables = JMeterContextService.getContext().getVariables(); if (!jMeterVariables.isSameUserOnNextIteration()) { - closeConnections(); + clearUserStores(); } } @@ -125,11 +128,44 @@ private void closeConnections() { clients.clear(); } + private void dump() { + Map clients = CONNECTIONS.get(); + for (HTTP2JettyClient client : clients.values()) { + try { + LOG.debug(client.dump()); + } catch (Exception e) { + LOG.error("Error while dump HTTP2JettyClient", e); + } + } + } + + @Override + public void testEnded() { + super.testEnded(); + HTTP2JettyClient.clearBufferPool(); + System.gc(); // Force free memory + } + @Override public void threadFinished() { + if (dumpAtThreadEnd) { + dump(); + } closeConnections(); } + private void clearUserStores() { + Map clients = CONNECTIONS.get(); + for (HTTP2JettyClient client : clients.values()) { + try { + client.clearCookies(); + client.clearAuthenticationResults(); + } catch (Exception e) { + LOG.error("Error while cleaning user store", e); + } + } + } + private static final class HTTP2ClientKey { private final String target; diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java index 0df1467..0a701ca 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java +++ b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientTest.java @@ -5,15 +5,16 @@ import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler; import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils; -import com.google.common.base.Stopwatch; import com.google.common.io.Resources; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -23,7 +24,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; @@ -165,7 +165,13 @@ private SslContextFactory.Server buildServerSslContextFactory() { } private String getKeyStorePath() { - return getClass().getResource("keystore.p12").getPath(); + try { + String[] arrPath = getClass().getResource("keystore.p12").toURI() + .toString().split(":/"); + return "/" + arrPath[arrPath.length - 1]; + } catch (URISyntaxException e) { + return null; + } } private Server buildServer(SslContextFactory.Server sslContextFactory) { @@ -260,6 +266,7 @@ private HTTPSampleResult sampleWithGet(String path) throws Exception { } private HTTPSampleResult sample(String path, String method) throws Exception { + client.loadProperties(); // Ensure to load the changes of properties in execution context return client.sample(sampler, buildBaseResult(createURL(path), method), false, 0); } @@ -274,19 +281,26 @@ private HTTPSampleResult buildBaseResult(URL url, String method) { return ret; } - @Test(expected = ExecutionException.class) - public void shouldThrowConnectExceptionWhenServerIsInaccessible() throws Exception { - client.sample(sampler, - buildBaseResult(new URL(HTTPConstants.PROTOCOL_HTTPS, HOST_NAME, 123, SERVER_PATH_200), - HTTPConstants.GET), false, 0); + @Test + public void shouldThrowConnectExceptionWhenServerIsInaccessible() { + try { + client.sample(sampler, + buildBaseResult(new URL(HTTPConstants.PROTOCOL_HTTPS, HOST_NAME, 123, SERVER_PATH_200), + HTTPConstants.GET), false, 0); + } catch (Exception ex) { + assert ((ex instanceof ExecutionException) || (ex instanceof TimeoutException)); + } } @Test public void shouldReturnSuccessSampleResultWhenSuccessResponseWithContentTypeGzip() throws Exception { buildStartedServer(); + HeaderManager hm = new HeaderManager(); + hm.add(new Header(HttpHeader.ACCEPT_ENCODING.asString(), "gzip")); + sampler.setHeaderManager(hm); HTTPSampleResult result = sampleWithGet(SERVER_PATH_200_GZIP); - assertThat(HTTP2JettyClientTest.BINARY_RESPONSE_BODY).isEqualTo(result.getResponseData()); + assertThat(result.getResponseHeaders().indexOf("content-encoding: gzip")).isNotEqualTo(-1); } @Test @@ -308,9 +322,7 @@ private HTTPSampleResult buildOkResult(String requestBody, String requestContent private HTTPSampleResult buildResult(boolean successful, HttpStatus.Code statusCode, HttpFields headers, byte[] requestBody, String requestContentType) { - Mutable httpFields = HttpFields.build() - .add(HttpHeader.ACCEPT_ENCODING, "gzip") - .add(HttpHeader.USER_AGENT, "Jetty/11.0.10"); + Mutable httpFields = HttpFields.build(); if (requestContentType != null) { httpFields.add(HttpHeader.CONTENT_TYPE, requestContentType); @@ -603,11 +615,12 @@ public void shouldGetOnlyRedirectedResultWhenRedirectAutomaticallyEnabledAndRedi @Test public void shouldGetFileDataWithFileIsSentAsBodyPart() throws Exception { buildStartedServer(); - URL file = getClass().getResource("blazemeter-labs-logo.png"); - HTTPFileArg fileArg = new HTTPFileArg(file.getPath(), "", "image/png"); - sampler.setHTTPFiles(new HTTPFileArg[]{fileArg}); + URL urlFile = getClass().getResource("blazemeter-labs-logo.png"); + String pathFile = new File(urlFile.getFile()).toPath().toAbsolutePath().toString(); + HTTPFileArg fileArg = new HTTPFileArg(pathFile, "", "image/png"); + sampler.setHTTPFiles(new HTTPFileArg[] {fileArg}); HTTPSampleResult result = sample(SERVER_PATH_200_FILE_SENT, HTTPConstants.POST); - HTTPSampleResult expected = buildResult(true, Code.OK, null, Resources.toByteArray(file), + HTTPSampleResult expected = buildResult(true, Code.OK, null, Resources.toByteArray(urlFile), "image/png"); validateResponse(result, expected); } @@ -730,7 +743,9 @@ private HTTPArgument buildArg(String name, String value) { } private HTTPFileArg buildFile(String name) { - String filePath = getClass().getResource("blazemeter-labs-logo.png").getPath(); + String filePath = + (new File(getClass().getResource("blazemeter-labs-logo.png").getFile())).toPath() + .toAbsolutePath().toString(); return new HTTPFileArg(filePath, name, "image/png"); } @@ -864,10 +879,11 @@ public void shouldUseMultipartWhenHasFilesAndNotSendAsPostBody() throws Exceptio @Test public void shouldNotUseMultipartWhenHasOneFileWithEmptyParamName() throws Exception { buildStartedServer(); - URL file = getClass().getResource("blazemeter-labs-logo.png"); - sampler.setHTTPFiles(new HTTPFileArg[]{new HTTPFileArg(file.getPath(), "", "image/png")}); + URL urlFile = getClass().getResource("blazemeter-labs-logo.png"); + String pathFile = new File(urlFile.getFile()).toPath().toAbsolutePath().toString(); + sampler.setHTTPFiles(new HTTPFileArg[] {new HTTPFileArg(pathFile, "", "image/png")}); HTTPSampleResult expected = buildResult(true, HttpStatus.Code.OK, null, - Resources.toByteArray(file), "image/png"); + Resources.toByteArray(urlFile), "image/png"); validateResponse(sample(SERVER_PATH_200_FILE_SENT, HTTPConstants.POST), expected); }