Skip to content

Commit 60b34a4

Browse files
committed
vaadin - New subsystem: session-cleaner
1 parent ac22f1b commit 60b34a4

13 files changed

Lines changed: 609 additions & 0 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 2.3.0
2+
* vaadin
3+
* New subsystem: `session-cleaner`
4+
* Tries to cleanup or minimize the data stored in VaadinSessions
5+
* Currently disabled by default - can be enabled with `sse.vaadin.session-cleaner.enabled=true`
6+
17
# 2.2.4
28
* oauth2-oidc
39
* Improve performance of `FastCookieFinder`

vaadin/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@
112112
<groupId>org.springframework.boot</groupId>
113113
<artifactId>spring-boot-autoconfigure</artifactId>
114114
</dependency>
115+
116+
<dependency>
117+
<groupId>org.junit.jupiter</groupId>
118+
<artifactId>junit-jupiter</artifactId>
119+
<version>6.0.3</version>
120+
<scope>test</scope>
121+
</dependency>
115122
</dependencies>
116123

117124
<build>
@@ -202,6 +209,12 @@
202209
</execution>
203210
</executions>
204211
</plugin>
212+
213+
<plugin>
214+
<groupId>org.apache.maven.plugins</groupId>
215+
<artifactId>maven-surefire-plugin</artifactId>
216+
<version>3.5.5</version>
217+
</plugin>
205218
</plugins>
206219
</build>
207220
<profiles>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright © 2025 XDEV Software (https://xdev.software)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package software.xdev.sse.vaadin.sessioncleaner;
17+
18+
public record ProcessedScheduleConfig(
19+
int initialDelaySec,
20+
int fixedDelaySec
21+
)
22+
{
23+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright © 2025 XDEV Software (https://xdev.software)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package software.xdev.sse.vaadin.sessioncleaner;
17+
18+
import java.util.Collections;
19+
import java.util.HashSet;
20+
import java.util.List;
21+
import java.util.Set;
22+
import java.util.concurrent.TimeUnit;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
import org.springframework.scheduling.annotation.Scheduled;
27+
import org.springframework.util.ConcurrentReferenceHashMap;
28+
29+
import com.vaadin.flow.server.VaadinService;
30+
import com.vaadin.flow.server.VaadinSession;
31+
32+
import software.xdev.sse.vaadin.sessioncleaner.task.VaadinSessionCleanerTask;
33+
34+
35+
/**
36+
* Periodically "cleans" Vaadin Sessions.
37+
* <p>
38+
* Vaadin - by default - only cleans up sessions when a request for them is received.<br/> However, there are multiple
39+
* scenarios where a client just abruptly stops sending requests, for example:
40+
* <ul>
41+
* <li>when network connectivity is lost</li>
42+
* <li>the device entered sleep mode/was shut down</li>
43+
* <li>the browser was force closed or crashed</li>
44+
* <li>Mobile devices - <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event">MDN</a>
45+
* </li>
46+
* </ul>
47+
* This might cause sessions to accumulate in memory which will result in a (kind of) memory leak.<br/>
48+
* The whole situation is especially problematic in situations where there is no authentication
49+
* before a VaadinSession is created.
50+
*/
51+
public class VaadinSessionCleaner
52+
{
53+
private static final Logger LOG = LoggerFactory.getLogger(VaadinSessionCleaner.class);
54+
55+
private final Set<VaadinSession> sessions = Collections.newSetFromMap(new ConcurrentReferenceHashMap<>());
56+
57+
private final List<VaadinSessionCleanerTask> tasks;
58+
59+
public VaadinSessionCleaner(final List<VaadinSessionCleanerTask> tasks)
60+
{
61+
this.tasks = tasks;
62+
63+
LOG.debug(
64+
"Instantiated with tasks: {}",
65+
tasks.stream()
66+
.map(VaadinSessionCleanerTask::getClass)
67+
.map(Class::getSimpleName)
68+
.toList());
69+
}
70+
71+
public static final String CLEANUP_SCHEDULE_CONFIG_BEAN_NAME = "vaadinSessionCleanerScheduleConfig";
72+
73+
@Scheduled(
74+
initialDelayString = "#{@" + CLEANUP_SCHEDULE_CONFIG_BEAN_NAME + ".initialDelaySec}",
75+
fixedRateString = "#{@" + CLEANUP_SCHEDULE_CONFIG_BEAN_NAME + ".fixedDelaySec}",
76+
timeUnit = TimeUnit.SECONDS)
77+
public void run()
78+
{
79+
LOG.debug("Starting cleanup");
80+
81+
final long startMs = System.currentTimeMillis();
82+
83+
// Defensive copy
84+
final Set<VaadinSession> vaadinSessions = new HashSet<>(this.sessions);
85+
for(final VaadinSession vaadinSession : vaadinSessions)
86+
{
87+
vaadinSession.lock();
88+
try
89+
{
90+
this.cleanUpSession(vaadinSession);
91+
}
92+
catch(final Exception ex)
93+
{
94+
LOG.warn("Failed to cleanup session[sessionId={}]", vaadinSession.getSession().getId(), ex);
95+
}
96+
finally
97+
{
98+
vaadinSession.unlock();
99+
}
100+
}
101+
102+
LOG.debug(
103+
"Finishes cleanup on {}x sessions, took {}ms",
104+
vaadinSessions.size(),
105+
System.currentTimeMillis() - startMs);
106+
}
107+
108+
protected void cleanUpSession(final VaadinSession vaadinSession)
109+
{
110+
final VaadinService vaadinService = vaadinSession.getService();
111+
if(vaadinService == null)
112+
{
113+
return;
114+
}
115+
116+
for(final VaadinSessionCleanerTask task : this.tasks)
117+
{
118+
try
119+
{
120+
task.cleanUp(vaadinService, vaadinSession);
121+
}
122+
catch(final Exception ex)
123+
{
124+
LOG.warn(
125+
"Failed to execute cleanup task[name={}] for session[sessionId={}]",
126+
task.getClass().getSimpleName(),
127+
vaadinSession.getSession().getId(),
128+
ex);
129+
}
130+
}
131+
}
132+
133+
public void install(final VaadinService vaadinService)
134+
{
135+
LOG.debug("Installing for {}", vaadinService);
136+
137+
vaadinService.addSessionInitListener(ev -> this.sessions.add(ev.getSession()));
138+
vaadinService.addSessionDestroyListener(ev -> this.sessions.remove(ev.getSession()));
139+
}
140+
}
141+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright © 2025 XDEV Software (https://xdev.software)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package software.xdev.sse.vaadin.sessioncleaner;
17+
18+
import com.vaadin.flow.server.ServiceInitEvent;
19+
import com.vaadin.flow.server.VaadinServiceInitListener;
20+
21+
22+
public class VaadinSessionCleanerServiceInitListener implements VaadinServiceInitListener
23+
{
24+
protected final VaadinSessionCleaner cleaner;
25+
26+
public VaadinSessionCleanerServiceInitListener(final VaadinSessionCleaner cleaner)
27+
{
28+
this.cleaner = cleaner;
29+
}
30+
31+
@Override
32+
public void serviceInit(final ServiceInitEvent event)
33+
{
34+
this.cleaner.install(event.getSource());
35+
}
36+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright © 2025 XDEV Software (https://xdev.software)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package software.xdev.sse.vaadin.sessioncleaner.auto;
17+
18+
import java.util.List;
19+
import java.util.Optional;
20+
21+
import org.springframework.beans.factory.annotation.Value;
22+
import org.springframework.boot.autoconfigure.AutoConfiguration;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.boot.context.properties.ConfigurationProperties;
26+
import org.springframework.context.annotation.Bean;
27+
28+
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
29+
30+
import software.xdev.sse.vaadin.sessioncleaner.ProcessedScheduleConfig;
31+
import software.xdev.sse.vaadin.sessioncleaner.VaadinSessionCleaner;
32+
import software.xdev.sse.vaadin.sessioncleaner.VaadinSessionCleanerServiceInitListener;
33+
import software.xdev.sse.vaadin.sessioncleaner.config.SessionCleanerConfig;
34+
import software.xdev.sse.vaadin.sessioncleaner.task.SessionUIsVaadinSessionCleanerTask;
35+
import software.xdev.sse.vaadin.sessioncleaner.task.UIFreeUpVaadinSessionCleanerTask;
36+
import software.xdev.sse.vaadin.sessioncleaner.task.VaadinSessionCleanerTask;
37+
38+
39+
@ConditionalOnProperty(value = "sse.vaadin.session-cleaner.enabled", matchIfMissing = false)
40+
@AutoConfiguration
41+
public class VaadinSessionCleanerAutoConfig
42+
{
43+
public static final double SCHEDULE_DEFAULT_INIT_DELAY_MULTIPLIER = 1.4;
44+
45+
@ConditionalOnMissingBean
46+
@Bean
47+
@ConfigurationProperties("sse.vaadin.session-cleaner")
48+
public SessionCleanerConfig sessionCleanerConfig()
49+
{
50+
return new SessionCleanerConfig();
51+
}
52+
53+
@ConditionalOnMissingBean
54+
@Bean(name = VaadinSessionCleaner.CLEANUP_SCHEDULE_CONFIG_BEAN_NAME)
55+
public ProcessedScheduleConfig vaadinSessionCleanerScheduleConfig(
56+
// @formatter:off
57+
@Value("${vaadin.heartbeatInterval:#{null}}")
58+
final Optional<Integer> optVaadinHeartbeatIntervalSec,
59+
@Value("${sse.vaadin.session-cleaner.schedule.initialDelaySec:#{null}}")
60+
final Optional<Integer> optInitialDelaySec,
61+
@Value("${sse.vaadin.session-cleaner.schedule.fixedDelaySec:#{null}}")
62+
final Optional<Integer> optFixedDelaySec
63+
// @formatter:on
64+
)
65+
{
66+
final int vaadinHeartbeatIntervalSec = optVaadinHeartbeatIntervalSec
67+
.orElse(DefaultDeploymentConfiguration.DEFAULT_HEARTBEAT_INTERVAL);
68+
69+
return new ProcessedScheduleConfig(
70+
optInitialDelaySec
71+
.orElseGet(() -> (int)(vaadinHeartbeatIntervalSec * SCHEDULE_DEFAULT_INIT_DELAY_MULTIPLIER)),
72+
optFixedDelaySec.orElse(vaadinHeartbeatIntervalSec)
73+
);
74+
}
75+
76+
@ConditionalOnProperty(
77+
value = "sse.vaadin.session-cleaner.tasks.session-uis.enabled",
78+
matchIfMissing = true)
79+
@ConditionalOnMissingBean
80+
@Bean
81+
public SessionUIsVaadinSessionCleanerTask sessionUIsVaadinSessionCleanerTask()
82+
{
83+
return new SessionUIsVaadinSessionCleanerTask();
84+
}
85+
86+
@ConditionalOnProperty(
87+
value = "sse.vaadin.session-cleaner.tasks.ui-freeup.enabled",
88+
matchIfMissing = true)
89+
@ConditionalOnMissingBean
90+
@Bean
91+
public UIFreeUpVaadinSessionCleanerTask uiFreeUpVaadinSessionCleanerTask(final SessionCleanerConfig config)
92+
{
93+
return new UIFreeUpVaadinSessionCleanerTask(config.getUiFreeUp());
94+
}
95+
96+
@ConditionalOnMissingBean
97+
@Bean
98+
public VaadinSessionCleaner vaadinSessionCleaner(final List<VaadinSessionCleanerTask> tasks)
99+
{
100+
return new VaadinSessionCleaner(tasks);
101+
}
102+
103+
@ConditionalOnMissingBean
104+
@Bean
105+
public VaadinSessionCleanerServiceInitListener vaadinSessionCleanerServiceInitListener(
106+
final SessionCleanerConfig config,
107+
final VaadinSessionCleaner cleaner)
108+
{
109+
return config.isInstallServiceInitListener()
110+
? new VaadinSessionCleanerServiceInitListener(cleaner)
111+
: null;
112+
}
113+
}

0 commit comments

Comments
 (0)