Skip to content

Commit 9c46f4a

Browse files
authored
Merge pull request #4699 from gchq/gh-4693-session-leak
PR for #4693 - sessionList is very slow v7.5 on a cluster
2 parents 636a739 + 21d5e01 commit 9c46f4a

File tree

14 files changed

+324
-100
lines changed

14 files changed

+324
-100
lines changed

stroom-app/src/main/java/stroom/app/App.java

+58-34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import stroom.config.app.AppConfig;
2626
import stroom.config.app.Config;
2727
import stroom.config.app.SecurityConfig;
28+
import stroom.config.app.SessionConfig;
29+
import stroom.config.app.SessionCookieConfig;
2830
import stroom.config.app.StroomYamlUtil;
2931
import stroom.config.global.impl.ConfigMapper;
3032
import stroom.dropwizard.common.AdminServlets;
@@ -54,7 +56,9 @@
5456
import stroom.util.logging.LambdaLoggerFactory;
5557
import stroom.util.logging.LogUtil;
5658
import stroom.util.shared.AbstractConfig;
59+
import stroom.util.shared.ModelStringUtil;
5760
import stroom.util.shared.ResourcePaths;
61+
import stroom.util.time.StroomDuration;
5862
import stroom.util.validation.ValidationModule;
5963
import stroom.util.yaml.YamlUtil;
6064

@@ -70,14 +74,14 @@
7074
import jakarta.inject.Inject;
7175
import jakarta.servlet.DispatcherType;
7276
import jakarta.servlet.FilterRegistration;
73-
import jakarta.servlet.SessionCookieConfig;
7477
import jakarta.validation.ValidatorFactory;
7578
import org.eclipse.jetty.server.session.SessionHandler;
7679
import org.eclipse.jetty.servlets.CrossOriginFilter;
7780

7881
import java.io.IOException;
7982
import java.nio.file.Path;
8083
import java.nio.file.Paths;
84+
import java.time.Duration;
8185
import java.util.EnumSet;
8286
import java.util.Objects;
8387

@@ -220,15 +224,14 @@ public void run(final Config configuration, final Environment environment) {
220224
// We want Stroom to use the root path so we need to move Dropwizard's path.
221225
environment.jersey().setUrlPattern(ResourcePaths.API_ROOT_PATH + "/*");
222226

223-
// Set up a session handler for Jetty
224-
configureSessionHandling(environment);
225-
226-
// Ensure the session cookie that provides JSESSIONID is secure.
227-
// Need to get it from ConfigMapper not AppConfig as ConfigMapper is now the source of
228-
// truth for config.
227+
// Need to get these config classed from ConfigMapper as the main appInjector is not created yet
228+
// and configuration only holds the YAML view of the config, not the DB view.
229229
final ConfigMapper configMapper = bootStrapInjector.getInstance(ConfigMapper.class);
230-
final stroom.config.app.SessionCookieConfig sessionCookieConfig = configMapper.getConfigObject(
231-
stroom.config.app.SessionCookieConfig.class);
230+
final SessionCookieConfig sessionCookieConfig = configMapper.getConfigObject(SessionCookieConfig.class);
231+
final SessionConfig sessionConfig = configMapper.getConfigObject(SessionConfig.class);
232+
233+
// Set up a session handler for Jetty
234+
configureSessionHandling(environment, sessionConfig);
232235
configureSessionCookie(environment, sessionCookieConfig);
233236

234237
// Configure Cross-Origin Resource Sharing.
@@ -273,11 +276,11 @@ public void run(final Config configuration, final Environment environment) {
273276

274277
private void showNodeInfo(final Config configuration) {
275278
LOGGER.info(""
276-
+ "\n********************************************************************************"
277-
+ "\n Stroom home: " + homeDirProvider.get().toAbsolutePath().normalize()
278-
+ "\n Stroom temp: " + tempDirProvider.get().toAbsolutePath().normalize()
279-
+ "\n Node name: " + getNodeName(configuration.getYamlAppConfig())
280-
+ "\n********************************************************************************");
279+
+ "\n********************************************************************************"
280+
+ "\n Stroom home: " + homeDirProvider.get().toAbsolutePath().normalize()
281+
+ "\n Stroom temp: " + tempDirProvider.get().toAbsolutePath().normalize()
282+
+ "\n Node name: " + getNodeName(configuration.getYamlAppConfig())
283+
+ "\n********************************************************************************");
281284
}
282285

283286
private void warnAboutDefaultOpenIdCreds(final Config configuration, final Injector injector) {
@@ -299,17 +302,17 @@ private void warnAboutDefaultOpenIdCreds(final Config configuration, final Injec
299302
.getFullPathStr(AbstractOpenIdConfig.PROP_NAME_IDP_TYPE);
300303

301304
LOGGER.warn("\n" +
302-
"\n -----------------------------------------------------------------------------" +
303-
"\n " +
304-
"\n WARNING!" +
305-
"\n " +
306-
"\n Using default and publicly available Open ID authentication credentials. " +
307-
"\n This is insecure! These should only be used in test/demo environments. " +
308-
"\n Set " + propPath + " to INTERNAL_IDP/EXTERNAL_IDP for production environments." +
309-
"\n" +
310-
"\n " + defaultOpenIdCredentials.getApiKey() +
311-
"\n -----------------------------------------------------------------------------" +
312-
"\n");
305+
"\n -----------------------------------------------------------------------------" +
306+
"\n " +
307+
"\n WARNING!" +
308+
"\n " +
309+
"\n Using default and publicly available Open ID authentication credentials. " +
310+
"\n This is insecure! These should only be used in test/demo environments. " +
311+
"\n Set " + propPath + " to INTERNAL_IDP/EXTERNAL_IDP for production environments." +
312+
"\n" +
313+
"\n " + defaultOpenIdCredentials.getApiKey() +
314+
"\n -----------------------------------------------------------------------------" +
315+
"\n");
313316
}
314317
}
315318

@@ -344,35 +347,56 @@ private void validateAppConfig(final Config config, final Path configFile) {
344347

345348
if (result.hasErrors() && appConfig.isHaltBootOnConfigValidationFailure()) {
346349
LOGGER.error("Application configuration is invalid. Stopping Stroom. To run Stroom with invalid " +
347-
"configuration, set {} to false, however this is not advised!",
350+
"configuration, set {} to false, however this is not advised!",
348351
appConfig.getFullPathStr(AppConfig.PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE));
349352
System.exit(1);
350353
}
351354
}
352355

353-
private static void configureSessionHandling(final Environment environment) {
354-
SessionHandler sessionHandler = new SessionHandler();
356+
private void configureSessionHandling(final Environment environment,
357+
final SessionConfig sessionConfig) {
358+
359+
final SessionHandler sessionHandler = new SessionHandler();
355360
// We need to give our session cookie a name other than JSESSIONID, otherwise it might
356361
// clash with other services running on the same domain.
357362
sessionHandler.setSessionCookie(SESSION_COOKIE_NAME);
363+
long maxInactiveIntervalSecs = NullSafe.getOrElse(
364+
sessionConfig.getMaxInactiveInterval(),
365+
StroomDuration::getDuration,
366+
Duration::toSeconds,
367+
-1L);
368+
if (maxInactiveIntervalSecs > Integer.MAX_VALUE) {
369+
maxInactiveIntervalSecs = -1;
370+
}
371+
LOGGER.info("Setting session maxInactiveInterval to {} secs ({})",
372+
ModelStringUtil.formatCsv(maxInactiveIntervalSecs),
373+
(maxInactiveIntervalSecs > 0
374+
? Duration.ofSeconds(maxInactiveIntervalSecs).toString()
375+
: String.valueOf(maxInactiveIntervalSecs)));
376+
377+
// If we don't let sessions expire then the Map of HttpSession in SessionListListener
378+
// will grow and grow
379+
sessionHandler.setMaxInactiveInterval((int) maxInactiveIntervalSecs);
380+
358381
environment.servlets().setSessionHandler(sessionHandler);
359382
environment.jersey().register(SessionFactoryProvider.class);
360383
}
361384

362-
private static void configureSessionCookie(final Environment environment,
363-
final stroom.config.app.SessionCookieConfig config) {
385+
private void configureSessionCookie(final Environment environment,
386+
final SessionCookieConfig sessionCookieConfig) {
364387
// Ensure the session cookie that provides JSESSIONID is secure.
365-
final SessionCookieConfig sessionCookieConfig = environment
388+
final jakarta.servlet.SessionCookieConfig servletSessionCookieConfig = environment
366389
.getApplicationContext()
367390
.getServletContext()
368391
.getSessionCookieConfig();
369-
sessionCookieConfig.setSecure(config.isSecure());
370-
sessionCookieConfig.setHttpOnly(config.isHttpOnly());
392+
servletSessionCookieConfig.setSecure(sessionCookieConfig.isSecure());
393+
servletSessionCookieConfig.setHttpOnly(sessionCookieConfig.isHttpOnly());
371394
// TODO : Add `SameSite=Strict` when supported by JEE
372395
}
373396

374397
private static void configureCors(io.dropwizard.core.setup.Environment environment) {
375-
FilterRegistration.Dynamic cors = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
398+
final FilterRegistration.Dynamic cors = environment.servlets()
399+
.addFilter("CORS", CrossOriginFilter.class);
376400
cors.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
377401
cors.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,PUT,POST,DELETE,OPTIONS,PATCH");
378402
cors.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*");

stroom-config/stroom-config-app/src/main/java/stroom/config/app/AppConfig.java

+22-10
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public class AppConfig extends AbstractConfig implements IsStroomConfig {
116116
public static final String PROP_NAME_SECURITY = "security";
117117
public static final String PROP_NAME_SERVICE_DISCOVERY = "serviceDiscovery";
118118
public static final String PROP_NAME_SESSION_COOKIE = "sessionCookie";
119+
public static final String PROP_NAME_SESSION = "session";
119120
public static final String PROP_NAME_SOLR = "solr";
120121
public static final String PROP_NAME_STATE = "state";
121122
public static final String PROP_NAME_STATISTICS = "statistics";
@@ -161,6 +162,7 @@ public class AppConfig extends AbstractConfig implements IsStroomConfig {
161162
private final SecurityConfig securityConfig;
162163
private final ServiceDiscoveryConfig serviceDiscoveryConfig;
163164
private final SessionCookieConfig sessionCookieConfig;
165+
private final SessionConfig sessionConfig;
164166
private final SolrConfig solrConfig;
165167
private final StateConfig stateConfig;
166168
private final StatisticsConfig statisticsConfig;
@@ -211,6 +213,7 @@ public AppConfig() {
211213
new SecurityConfig(),
212214
new ServiceDiscoveryConfig(),
213215
new SessionCookieConfig(),
216+
new SessionConfig(),
214217
new SolrConfig(),
215218
new StateConfig(),
216219
new StatisticsConfig(),
@@ -260,6 +263,7 @@ public AppConfig(@JsonProperty(PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE)
260263
@JsonProperty(PROP_NAME_SECURITY) final SecurityConfig securityConfig,
261264
@JsonProperty(PROP_NAME_SERVICE_DISCOVERY) final ServiceDiscoveryConfig serviceDiscoveryConfig,
262265
@JsonProperty(PROP_NAME_SESSION_COOKIE) final SessionCookieConfig sessionCookieConfig,
266+
@JsonProperty(PROP_NAME_SESSION) final SessionConfig sessionConfig,
263267
@JsonProperty(PROP_NAME_SOLR) final SolrConfig solrConfig,
264268
@JsonProperty(PROP_NAME_STATE) final StateConfig stateConfig,
265269
@JsonProperty(PROP_NAME_STATISTICS) final StatisticsConfig statisticsConfig,
@@ -305,6 +309,7 @@ public AppConfig(@JsonProperty(PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE)
305309
this.securityConfig = securityConfig;
306310
this.serviceDiscoveryConfig = serviceDiscoveryConfig;
307311
this.sessionCookieConfig = sessionCookieConfig;
312+
this.sessionConfig = sessionConfig;
308313
this.solrConfig = solrConfig;
309314
this.stateConfig = stateConfig;
310315
this.statisticsConfig = statisticsConfig;
@@ -317,11 +322,12 @@ public AppConfig(@JsonProperty(PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE)
317322

318323
@AssertTrue(
319324
message = "stroom." + PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE + " is set to false. If there is " +
320-
"invalid configuration the system may behave in unexpected ways. This setting is not advised.",
325+
"invalid configuration the system may behave in unexpected ways. This setting is not advised.",
321326
payload = ValidationSeverity.Warning.class)
322327
@JsonProperty(PROP_NAME_HALT_BOOT_ON_CONFIG_VALIDATION_FAILURE)
323328
@JsonPropertyDescription("If true, Stroom will halt on start up if any errors are found in the YAML " +
324-
"configuration file. If false, the errors will simply be logged. Setting this to false is not advised.")
329+
"configuration file. If false, the errors will simply be logged." +
330+
"Setting this to false is not advised.")
325331
public boolean isHaltBootOnConfigValidationFailure() {
326332
return haltBootOnConfigValidationFailure;
327333
}
@@ -363,8 +369,9 @@ public ClusterLockConfig getClusterLockConfig() {
363369

364370
@JsonProperty(PROP_NAME_COMMON_DB_DETAILS)
365371
@JsonPropertyDescription("Defines a set of common database connection details to use if no connection details " +
366-
"are defined for a service area in stroom, e.g. core or config. This means you can have all service " +
367-
"areas running in a single database, have each in their own database or a mixture.")
372+
"are defined for a service area in stroom, e.g. core or config. This means you can " +
373+
"have all service areas running in a single database, have each in their own " +
374+
"database or a mixture.")
368375
public CommonDbConfig getCommonDbConfig() {
369376
return commonDbConfig;
370377
}
@@ -448,10 +455,10 @@ public NodeConfig getNodeConfig() {
448455
}
449456

450457
@JsonPropertyDescription("This is the base endpoint of the node for all inter-node communications, " +
451-
"i.e. all cluster management and node info calls. " +
452-
"This endpoint will typically be hidden behind a firewall and not be publicly available. " +
453-
"The address must be resolvable from all other nodes in the cluster. " +
454-
"This does not need to be set for a single node cluster.")
458+
"i.e. all cluster management and node info calls. " +
459+
"This endpoint will typically be hidden behind a firewall and not be " +
460+
"publicly available. The address must be resolvable from all other nodes " +
461+
"in the cluster. This does not need to be set for a single node cluster.")
455462
@JsonProperty(PROP_NAME_NODE_URI)
456463
public NodeUriConfig getNodeUri() {
457464
return nodeUri;
@@ -479,7 +486,7 @@ public PropertyServiceConfig getPropertyServiceConfig() {
479486
}
480487

481488
@JsonPropertyDescription("This is public facing URI of stroom which may be different from the local host if " +
482-
"behind a proxy")
489+
"behind a proxy")
483490
@JsonProperty(PROP_NAME_PUBLIC_URI)
484491
public PublicUriConfig getPublicUri() {
485492
return publicUri;
@@ -537,6 +544,11 @@ public SessionCookieConfig getSessionCookieConfig() {
537544
return sessionCookieConfig;
538545
}
539546

547+
@JsonProperty(PROP_NAME_SESSION)
548+
public SessionConfig getSessionConfig() {
549+
return sessionConfig;
550+
}
551+
540552
@JsonProperty(PROP_NAME_STATE)
541553
@JsonPropertyDescription("Configuration for the stroom state service")
542554
public StateConfig getStateConfig() {
@@ -555,7 +567,7 @@ public UiConfig getUiConfig() {
555567
}
556568

557569
@JsonPropertyDescription("This is the URI where the UI is hosted if different to the public facing URI of the " +
558-
"server, e.g. during development or some other deployments")
570+
"server, e.g. during development or some other deployments")
559571
@JsonProperty(PROP_NAME_UI_URI)
560572
public UiUriConfig getUiUri() {
561573
return uiUri;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package stroom.config.app;
2+
3+
import stroom.util.config.annotations.RequiresRestart;
4+
import stroom.util.config.annotations.RequiresRestart.RestartScope;
5+
import stroom.util.shared.AbstractConfig;
6+
import stroom.util.shared.IsStroomConfig;
7+
import stroom.util.time.StroomDuration;
8+
9+
import com.fasterxml.jackson.annotation.JsonCreator;
10+
import com.fasterxml.jackson.annotation.JsonProperty;
11+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
12+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
13+
14+
15+
@JsonPropertyOrder(alphabetic = true)
16+
public class SessionConfig extends AbstractConfig implements IsStroomConfig {
17+
18+
public static final String PROP_NAME_MAX_INACTIVE_INTERVAL = "maxInactiveInterval";
19+
20+
public static final StroomDuration DEFAULT_MAX_INACTIVE_INTERVAL = StroomDuration.ofDays(1);
21+
22+
private final StroomDuration maxInactiveInterval;
23+
24+
public SessionConfig() {
25+
this.maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL;
26+
}
27+
28+
@SuppressWarnings("unused")
29+
@JsonCreator
30+
public SessionConfig(
31+
@JsonProperty(PROP_NAME_MAX_INACTIVE_INTERVAL) final StroomDuration maxInactiveInterval) {
32+
this.maxInactiveInterval = maxInactiveInterval;
33+
}
34+
35+
@RequiresRestart(RestartScope.UI)
36+
@JsonProperty(PROP_NAME_MAX_INACTIVE_INTERVAL)
37+
@JsonPropertyDescription("The maximum time interval between the last access of a HTTP session and " +
38+
"it being considered expired. Set to null for sessions that never expire, " +
39+
"however this will causes sessions to be held and build up in memory indefinitely, " +
40+
"so is best avoided.")
41+
public StroomDuration getMaxInactiveInterval() {
42+
return maxInactiveInterval;
43+
}
44+
45+
@Override
46+
public String toString() {
47+
return "SessionConfig{" +
48+
"maxInactiveInterval=" + maxInactiveInterval +
49+
'}';
50+
}
51+
}

stroom-config/stroom-config-app/src/test/resources/stroom/config/app/expected.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ appConfig:
926926
servicesPort: 8080
927927
zookeeperBasePath: "/stroom-services"
928928
zookeeperUrl: "localhost:2181"
929+
session:
930+
maxInactiveInterval: "P1D"
929931
sessionCookie:
930932
httpOnly: true
931933
secure: true

0 commit comments

Comments
 (0)