Make session-sweeping non-blocking

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-09-17 12:13:43 +02:00 • committed by Daniel Garnier-Moiroux
commit caafc85575

View file

@ -216,17 +216,14 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* down * down
*/ */
private Disposable startSessionSweeper(Duration sessionSweepInterval) { private Disposable startSessionSweeper(Duration sessionSweepInterval) {
return Flux.interval(sessionSweepInterval, sessionSweepInterval, Schedulers.boundedElastic()).doOnNext(tick -> { return Flux.interval(sessionSweepInterval, sessionSweepInterval, Schedulers.boundedElastic())
// Each sweep runs in its own subscription, so that a sweep failing does .concatMap(
// not terminate the interval and disable sweeping altogether tick -> sweepSessions().doOnError(e -> logger.error("Session sweep failed", e)).onErrorComplete())
Mono.fromRunnable(this::sweepSessions) .onErrorComplete(error -> {
.doOnError(e -> logger.error("Session sweep failed", e)) logger.error("Session sweeper error", error);
.onErrorComplete() return true;
.subscribe(); })
}).onErrorComplete(error -> { .subscribe();
logger.error("Session sweeper error", error);
return true;
}).subscribe();
} }
/** /**
@ -235,25 +232,23 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
* if it received a request during the interval which just elapsed. Anything else is a * if it received a request during the interval which just elapsed. Anything else is a
* session whose client went away without deleting it: the protocol lets a client * session whose client went away without deleting it: the protocol lets a client
* reconnect to a session, so nothing else ever reclaims it. * reconnect to a session, so nothing else ever reclaims it.
* @return a Mono completing once every evicted session has been closed
*/ */
private void sweepSessions() { private Mono<Void> sweepSessions() {
if (this.isClosing) { if (this.isClosing) {
return; return Mono.empty();
} }
Set<String> active = this.activeSessions.getAndSet(ConcurrentHashMap.newKeySet()); Set<String> active = this.activeSessions.getAndSet(ConcurrentHashMap.newKeySet());
this.sessions.values().removeIf(session -> { return Flux.fromIterable(this.sessions.values())
if (session.hasOpenStream() || active.contains(session.getId())) { .filter(session -> !session.hasOpenStream() && !active.contains(session.getId()))
return false; .filter(session -> this.sessions.remove(session.getId(), session))
} .flatMap(session -> {
logger.debug("Evicting idle session {}", session.getId()); logger.debug("Evicting idle session {}", session.getId());
try { return session.closeGracefully()
session.closeGracefully().block(); .doOnError(e -> logger.warn("Failed to close idle session {}: {}", session.getId(), e.getMessage()))
} .onErrorComplete();
catch (Exception e) { })
logger.warn("Failed to close idle session {}: {}", session.getId(), e.getMessage()); .then();
}
return true;
});
} }
/** /**