Skip to content

Commit 858329e

Browse files
Merge pull request #300 from rsiatka/master
Add support for connection Keep-Alive
2 parents e7cbcc9 + 6abd987 commit 858329e

File tree

6 files changed

+222
-1
lines changed

6 files changed

+222
-1
lines changed

docs/guide/protocols/http/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Here we show some of them, but check [JmeterDsl](/jmeter-java-dsl/src/main/java/
1414
<!-- @include: connections.md -->
1515
<!-- @include: embedded-resources.md -->
1616
<!-- @include: redirects.md -->
17+
<!-- @include: keepAlive.md -->
1718
<!-- @include: defaults.md -->
1819
<!-- @include: override-defaults.md -->
1920
<!-- @include: proxy.md -->
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#### Keep alive
2+
3+
Jmeter-java-dsl maintains a persistent http connection for subsequent requests to the same server.
4+
It is done by sending `Connection: keep-alive` header.
5+
If you want to disable keep-alive logic and force a server to close connection after each request then use`.useKeepAlive(false)` in a given `httpSampler` or `httpDefaults`.
6+
7+
8+
```java
9+
import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
10+
import org.junit.jupiter.api.Test;
11+
public class PerformanceTest {
12+
13+
@Test
14+
public void test1() throws Exception {
15+
testPlan(
16+
threadGroup(1, 10,
17+
httpSampler("https://myservice1.com")
18+
.useKeepAlive(false),
19+
httpSampler("https://myservice2.com")
20+
.useKeepAlive(true),
21+
httpSampler("https://myservice3.com")
22+
)
23+
).run();
24+
}
25+
26+
@Test
27+
public void test2() throws Exception {
28+
testPlan(
29+
httpDefaults()
30+
.useKeepAlive(false),
31+
threadGroup(1, 10,
32+
httpSampler("https://myservice1.com"),
33+
httpSampler("https://myservice2.com")
34+
)
35+
).run();
36+
}
37+
38+
}
39+
```

jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpDefaults.java

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public class DslHttpDefaults extends BaseConfigElement {
5353
protected String proxyUser;
5454
protected String proxyPassword;
5555
protected HttpClientImpl clientImpl;
56+
protected Boolean useKeepAlive;
5657
protected Boolean followRedirects;
5758

5859
public DslHttpDefaults() {
@@ -189,6 +190,18 @@ public DslHttpDefaults followRedirects(boolean enable) {
189190
return this;
190191
}
191192

193+
/**
194+
* Specifies if by default HTTP connection should be kept alive.
195+
*
196+
* @param enable sets either to enable or disable persistent connection
197+
* @return the config element for further configuration or usage.
198+
* @since 1.30
199+
*/
200+
public DslHttpDefaults useKeepAlive(boolean enable) {
201+
this.useKeepAlive = enable;
202+
return this;
203+
}
204+
192205
/**
193206
* Allows enabling automatic download of HTML embedded resources (images, iframes, etc) by
194207
* default.
@@ -430,6 +443,9 @@ public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) {
430443
if (followRedirects != null) {
431444
buildEndListener(context.getParent()).followRedirects = followRedirects;
432445
}
446+
if (useKeepAlive != null) {
447+
buildEndListener(context.getParent()).useKeepAlive = useKeepAlive;
448+
}
433449
return ret;
434450
}
435451

@@ -445,11 +461,19 @@ protected static void addPendingFollowRedirectsElement(HTTPSamplerProxy element,
445461
buildEndListener(context.getRoot()).pendingFollowRedirectsElements.add(element);
446462
}
447463

464+
protected static void addPendingUseKeepAliveElement(HTTPSamplerProxy element,
465+
BuildTreeContext context) {
466+
buildEndListener(context.getRoot()).pendingUseKeepAliveElements.add(element);
467+
}
468+
448469
private static class DefaultsTreeContextEndListener implements TreeContextEndListener {
449470

450471
private Boolean followRedirects;
451472
private final List<HTTPSamplerProxy> pendingFollowRedirectsElements = new ArrayList<>();
452473

474+
private Boolean useKeepAlive;
475+
private final List<HTTPSamplerProxy> pendingUseKeepAliveElements = new ArrayList<>();
476+
453477
private DefaultsTreeContextEndListener(BuildTreeContext context) {
454478
context.addEndListener(this);
455479
}
@@ -463,6 +487,14 @@ public void execute(BuildTreeContext context, HashTree tree) {
463487
.filter(e -> e.getPropertyAsString(HTTPSamplerBase.FOLLOW_REDIRECTS).isEmpty())
464488
.forEach(e -> e.setFollowRedirects(true));
465489
}
490+
491+
if (useKeepAlive != null) {
492+
setChildrenToUseKeepAlive(tree);
493+
} else {
494+
pendingUseKeepAliveElements.stream()
495+
.filter(e -> e.getPropertyAsString(HTTPSamplerBase.USE_KEEPALIVE).isEmpty())
496+
.forEach(e -> e.setUseKeepAlive(true));
497+
}
466498
}
467499

468500
private void setChildrenToFollowRedirects(HashTree tree) {
@@ -478,6 +510,19 @@ private void setChildrenToFollowRedirects(HashTree tree) {
478510
});
479511
}
480512

513+
private void setChildrenToUseKeepAlive(HashTree tree) {
514+
tree.forEach((key, value) -> {
515+
if (key instanceof HTTPSamplerProxy) {
516+
HTTPSamplerProxy sampler = (HTTPSamplerProxy) key;
517+
if (sampler.getPropertyAsString(HTTPSamplerBase.USE_KEEPALIVE).isEmpty()) {
518+
sampler.setUseKeepAlive(useKeepAlive);
519+
}
520+
} else {
521+
setChildrenToUseKeepAlive(value);
522+
}
523+
});
524+
}
525+
481526
}
482527

483528
public static class CodeBuilder extends SingleGuiClassCallBuilder {
@@ -524,12 +569,16 @@ public void registerDependency(MethodCallContext context) {
524569
boolean followRedirects =
525570
isDefaultCandidate || testElement.getPropertyAsBoolean(HTTPSamplerBase.FOLLOW_REDIRECTS);
526571
endListener.registerFollowRedirect(followRedirects, context, isDefaultCandidate);
572+
boolean useKeepAlive =
573+
isDefaultCandidate || testElement.getPropertyAsBoolean(HTTPSamplerBase.USE_KEEPALIVE);
574+
endListener.registerUseKeepAlive(useKeepAlive, context, isDefaultCandidate);
527575
}
528576

529577
private static class DefaultsMethodContextEndListener implements MethodCallContextEndListener {
530578

531579
private final List<EncodedCall> encodedCalls = new ArrayList<>();
532580
private final List<RedirectableCall> redirectableCalls = new ArrayList<>();
581+
private final List<UseKeepAliveCall> useKeepAliveCalls = new ArrayList<>();
533582

534583
private DefaultsMethodContextEndListener(MethodCallContext parentCtx) {
535584
parentCtx.addEndListener(this);
@@ -545,10 +594,16 @@ public void registerFollowRedirect(boolean followRedirects, MethodCallContext re
545594
redirectableCalls.add(new RedirectableCall(followRedirects, ret, isDefaultCandidate));
546595
}
547596

597+
public void registerUseKeepAlive(boolean useKeepAlive, MethodCallContext ret,
598+
boolean isDefaultCandidate) {
599+
useKeepAliveCalls.add(new UseKeepAliveCall(useKeepAlive, ret, isDefaultCandidate));
600+
}
601+
548602
@Override
549603
public void execute(MethodCallContext ctx, MethodCall ret) {
550604
solveDefaultEncoding(ctx);
551605
solveDefaultFollowRedirects(ctx);
606+
solveDefaultUseKeepAlive(ctx);
552607
}
553608

554609
private void solveDefaultEncoding(MethodCallContext ctx) {
@@ -623,6 +678,21 @@ private void solveDefaultFollowRedirects(MethodCallContext ctx) {
623678
}
624679
}
625680

681+
private void solveDefaultUseKeepAlive(MethodCallContext ctx) {
682+
UseKeepAliveCall defaultsCall = findDefaultUseKeepAliveCall(ctx);
683+
if (defaultsCall != null) {
684+
useKeepAliveCalls.stream()
685+
.filter(c -> c != defaultsCall)
686+
.forEach(UseKeepAliveCall::removeUseKeepAlive);
687+
}
688+
DefaultsMethodContextEndListener parentListener = buildParentContextEndListener(ctx);
689+
if (parentListener != null) {
690+
parentListener.registerUseKeepAlive(
691+
defaultsCall == null || defaultsCall.useKeepAlive,
692+
defaultsCall != null ? defaultsCall.ctx : null, false);
693+
}
694+
}
695+
626696
private RedirectableCall findDefaultRedirectableCall(MethodCallContext ctx) {
627697
if (redirectableCalls.size() == 1) {
628698
return redirectableCalls.get(0);
@@ -641,6 +711,24 @@ private RedirectableCall findDefaultRedirectableCall(MethodCallContext ctx) {
641711
return null;
642712
}
643713

714+
private UseKeepAliveCall findDefaultUseKeepAliveCall(MethodCallContext ctx) {
715+
if (useKeepAliveCalls.size() == 1) {
716+
return useKeepAliveCalls.get(0);
717+
}
718+
if (useKeepAliveCalls.stream().allMatch(c -> c.isDefaultCandidate || !c.useKeepAlive)) {
719+
UseKeepAliveCall ret = useKeepAliveCalls.stream()
720+
.filter(c -> c.isDefaultCandidate)
721+
.findAny()
722+
.orElse(null);
723+
if (ret == null) {
724+
ret = new UseKeepAliveCall(false, buildDefaultsCall(ctx), true);
725+
}
726+
ret.setUseKeepAlive(false);
727+
return ret;
728+
}
729+
return null;
730+
}
731+
644732
}
645733

646734
}
@@ -702,4 +790,29 @@ public void removeFollowRedirects() {
702790

703791
}
704792

793+
private static class UseKeepAliveCall {
794+
795+
private boolean useKeepAlive;
796+
private final MethodCallContext ctx;
797+
private final boolean isDefaultCandidate;
798+
799+
private UseKeepAliveCall(boolean useKeepAlive, MethodCallContext ctx,
800+
boolean isDefaultCandidate) {
801+
this.useKeepAlive = useKeepAlive;
802+
this.ctx = ctx;
803+
this.isDefaultCandidate = isDefaultCandidate;
804+
}
805+
806+
public void setUseKeepAlive(boolean useKeepAlive) {
807+
this.useKeepAlive = useKeepAlive;
808+
ctx.getMethodCall().chain("useKeepAlive", new BoolParam(useKeepAlive, true));
809+
}
810+
811+
public void removeUseKeepAlive() {
812+
ctx.getMethodCall().unchain("useKeepAlive");
813+
removeEmptyDefaultsCall(ctx);
814+
}
815+
816+
}
817+
705818
}

jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpSampler.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class DslHttpSampler extends DslBaseHttpSampler<DslHttpSampler> {
5050
protected final List<HTTPFileArg> files = new ArrayList<>();
5151
protected Charset encoding;
5252
protected Boolean followRedirects;
53+
protected Boolean useKeepAlive;
5354
protected boolean downloadEmbeddedResources;
5455
protected String embeddedResourcesMatchRegex;
5556
protected String embeddedResourcesNotMatchRegex;
@@ -299,6 +300,23 @@ public DslHttpSampler followRedirects(boolean followRedirects) {
299300
return this;
300301
}
301302

303+
/**
304+
* Allows to close http connection or keep it alive.
305+
* <p>
306+
* By default, JMeter sends `Connection: keep-alive` header to enable persistent connection.
307+
* To change default behavior and close every connection
308+
* http client should send `Connection: close` header.
309+
* This method allows enabling/disabling such behavior.
310+
*
311+
* @param useKeepAlive sets either to enable or disable persistent connection
312+
* @return the sampler for further configuration or usage.
313+
* @since 1.30
314+
*/
315+
public DslHttpSampler useKeepAlive(boolean useKeepAlive) {
316+
this.useKeepAlive = useKeepAlive;
317+
return this;
318+
}
319+
302320
/**
303321
* Allows enabling automatic download of HTML embedded resources (images, iframes, etc).
304322
* <p>
@@ -422,7 +440,9 @@ with solved entries (like FunctionProperty)
422440
if (followRedirects != null) {
423441
elem.setFollowRedirects(followRedirects);
424442
}
425-
elem.setUseKeepAlive(true);
443+
if (useKeepAlive != null) {
444+
elem.setUseKeepAlive(useKeepAlive);
445+
}
426446
HttpElementHelper.modifyTestElementEmbeddedResources(elem, downloadEmbeddedResources,
427447
embeddedResourcesMatchRegex, embeddedResourcesNotMatchRegex);
428448
if (clientImpl != null) {
@@ -445,6 +465,13 @@ public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) {
445465
*/
446466
DslHttpDefaults.addPendingFollowRedirectsElement(element, context);
447467
}
468+
if (useKeepAlive == null) {
469+
/*
470+
Not setting keep alive default value in buildTestElement and differing it, allows for
471+
httpDefaults to define connection behavior for elements that have not specified a value.
472+
*/
473+
DslHttpDefaults.addPendingUseKeepAliveElement(element, context);
474+
}
448475
return ret;
449476
}
450477

@@ -511,6 +538,7 @@ protected MethodCall buildBaseHttpMethodCall(MethodParam name, MethodParam url,
511538
protected void chainAdditionalOptions(MethodCall ret, TestElementParamBuilder paramBuilder) {
512539
HttpElementHelper.chainEncodingToMethodCall(ret, paramBuilder);
513540
ret.chain("followRedirects", buildFollowRedirectsParam(paramBuilder));
541+
ret.chain("useKeepAlive", buildUseKeepAliveParam(paramBuilder));
514542
HttpElementHelper.chainEmbeddedResourcesOptionsToMethodCall(ret, paramBuilder);
515543
HttpElementHelper.chainClientImplToMethodCall(ret, paramBuilder);
516544
}
@@ -599,6 +627,10 @@ private MethodParam buildFollowRedirectsParam(TestElementParamBuilder paramBuild
599627
}
600628
}
601629

630+
private MethodParam buildUseKeepAliveParam(TestElementParamBuilder paramBuilder) {
631+
return paramBuilder.boolParam(HTTPSamplerBase.USE_KEEPALIVE, true);
632+
}
633+
602634
private static class HttpMethodParam extends StringParam {
603635

604636
private static final Map<String, String> CONSTANT_METHODS = findConstantNamesMap(

jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/http/DslHttpDefaultsTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
88
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
99
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
10+
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
11+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
1012
import static org.assertj.core.api.Assertions.assertThat;
1113
import static us.abstracta.jmeter.javadsl.JmeterDsl.httpDefaults;
1214
import static us.abstracta.jmeter.javadsl.JmeterDsl.httpSampler;
@@ -205,6 +207,19 @@ private void setupMockedRedirectionTo(String redirectPath) {
205207
.withHeader("Location", wiremockUri + redirectPath)));
206208
}
207209

210+
@Test
211+
public void shouldNotSendConnectionKeepAliveWhenDisabledInDefaultsAndNotSettingInSampler()
212+
throws Exception {
213+
testPlan(
214+
httpDefaults()
215+
.useKeepAlive(false),
216+
threadGroup(1, 1,
217+
httpSampler(wiremockUri)
218+
)
219+
).run();
220+
verify(getRequestedFor(anyUrl()).withHeader("Connection", equalTo("close")));
221+
}
222+
208223
@Test
209224
public void shouldShowInGuiWhenShowInGui() {
210225
Robot robot = BasicRobot.robotWithNewAwtHierarchy();

jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/http/DslHttpSamplerTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ public void shouldNotFollowRedirectsWhenHttpSamplerWithDisabledFollowRedirects()
112112
verify(0, getRequestedFor(urlPathEqualTo(REDIRECT_PATH)));
113113
}
114114

115+
@Test
116+
public void shouldNotSendConnectionKeepAliveWhenHttpSamplerWithDisabledKeepAlive()
117+
throws Exception {
118+
testPlan(
119+
threadGroup(1, 1,
120+
httpSampler(wiremockUri)
121+
.useKeepAlive(false)
122+
)
123+
).run();
124+
verify(getRequestedFor(anyUrl()).withHeader("Connection", equalTo("close")));
125+
}
126+
115127
@Test
116128
public void shouldSendHeadersWhenHttpSamplerWithHeaders() throws Exception {
117129
testPlan(
@@ -646,6 +658,15 @@ public DslTestPlan testPlanWithHttpGetNotFollowingRedirects() {
646658
);
647659
}
648660

661+
public DslTestPlan testPlanWithHttpGetNotUsingKeepAlive() {
662+
return testPlan(
663+
threadGroup(1, 1,
664+
httpSampler("http://localhost")
665+
.useKeepAlive(false)
666+
)
667+
);
668+
}
669+
649670
public DslTestPlan testPlanWithHttpGetAndEmbeddedResourcesDownload() {
650671
return testPlan(
651672
threadGroup(1, 1,

0 commit comments

Comments
 (0)