From 950e787885fa98006b9732b543c91efd00e59fc1 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Fri, 15 May 2026 06:42:08 -0700 Subject: [PATCH] Periodic session timeout sweep --- .../org/traccar/schedule/ScheduleManager.java | 1 + .../traccar/schedule/TaskSessionTimeout.java | 47 ++++++++++++++ .../traccar/session/ConnectionManager.java | 65 ++++++++++--------- 3 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/traccar/schedule/TaskSessionTimeout.java diff --git a/src/main/java/org/traccar/schedule/ScheduleManager.java b/src/main/java/org/traccar/schedule/ScheduleManager.java index fd8171148..74e0e1a44 100644 --- a/src/main/java/org/traccar/schedule/ScheduleManager.java +++ b/src/main/java/org/traccar/schedule/ScheduleManager.java @@ -50,6 +50,7 @@ public class ScheduleManager implements LifecycleObject { TaskDeleteTemporary.class, TaskReports.class, TaskDeviceInactivityCheck.class, + TaskSessionTimeout.class, TaskWebSocketKeepalive.class) .forEachOrdered(taskClass -> { var task = injector.getInstance(taskClass); diff --git a/src/main/java/org/traccar/schedule/TaskSessionTimeout.java b/src/main/java/org/traccar/schedule/TaskSessionTimeout.java new file mode 100644 index 000000000..34a09f0d0 --- /dev/null +++ b/src/main/java/org/traccar/schedule/TaskSessionTimeout.java @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Anton Tananaev (anton@traccar.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.traccar.schedule; + +import jakarta.inject.Inject; +import org.traccar.config.Config; +import org.traccar.config.Keys; +import org.traccar.session.ConnectionManager; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TaskSessionTimeout implements ScheduleTask { + + private final ConnectionManager connectionManager; + private final long period; + + @Inject + public TaskSessionTimeout(Config config, ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + period = Math.max(config.getLong(Keys.STATUS_TIMEOUT) / 10, 1); + } + + @Override + public void schedule(ScheduledExecutorService executor) { + executor.scheduleAtFixedRate(this, period, period, TimeUnit.SECONDS); + } + + @Override + public void run() { + connectionManager.sweepIdleSessions(); + } + +} diff --git a/src/main/java/org/traccar/session/ConnectionManager.java b/src/main/java/org/traccar/session/ConnectionManager.java index e4e616e69..45723ea9d 100644 --- a/src/main/java/org/traccar/session/ConnectionManager.java +++ b/src/main/java/org/traccar/session/ConnectionManager.java @@ -16,8 +16,6 @@ package org.traccar.session; import io.netty.channel.Channel; -import io.netty.util.Timeout; -import io.netty.util.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.traccar.Protocol; @@ -54,7 +52,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Singleton @@ -77,7 +74,6 @@ public class ConnectionManager implements BroadcastInterface { private final CacheManager cacheManager; private final Storage storage; private final NotificationManager notificationManager; - private final Timer timer; private final BroadcastService broadcastService; private final DeviceLookupService deviceLookupService; @@ -85,18 +81,15 @@ public class ConnectionManager implements BroadcastInterface { private final Map> userDevices = new HashMap<>(); private final Map> deviceUsers = new HashMap<>(); - private final Map timeouts = new ConcurrentHashMap<>(); - @Inject public ConnectionManager( Config config, CacheManager cacheManager, Storage storage, - NotificationManager notificationManager, Timer timer, BroadcastService broadcastService, + NotificationManager notificationManager, BroadcastService broadcastService, DeviceLookupService deviceLookupService) { this.config = config; this.cacheManager = cacheManager; this.storage = storage; this.notificationManager = notificationManager; - this.timer = timer; this.broadcastService = broadcastService; this.deviceLookupService = deviceLookupService; deviceTimeout = config.getLong(Keys.STATUS_TIMEOUT); @@ -105,6 +98,16 @@ public class ConnectionManager implements BroadcastInterface { broadcastService.registerListener(this); } + public void sweepIdleSessions() { + long cutoff = System.currentTimeMillis() - deviceTimeout * 1000; + for (DeviceSession session : sessionsByDeviceId.values()) { + if (session.getLastUpdate() < cutoff) { + deviceUnknown(session.getDeviceId()); + } + } + unknownByEndpoint.values().removeIf(entry -> entry.timestamp() < cutoff); + } + public DeviceSession getDeviceSession(long deviceId) { return sessionsByDeviceId.get(deviceId); } @@ -114,19 +117,28 @@ public class ConnectionManager implements BroadcastInterface { String... uniqueIds) throws Exception { ConnectionKey connectionKey = new ConnectionKey(channel, remoteAddress); - Map endpointSessions = sessionsByEndpoint.computeIfAbsent( - connectionKey, k -> new ConcurrentHashMap<>()); + Map endpointSessions = sessionsByEndpoint.get(connectionKey); uniqueIds = Arrays.stream(uniqueIds).filter(Objects::nonNull).toArray(String[]::new); if (uniqueIds.length > 0) { - for (String uniqueId : uniqueIds) { - DeviceSession deviceSession = endpointSessions.get(uniqueId); - if (deviceSession != null) { - return deviceSession; + if (endpointSessions != null) { + for (String uniqueId : uniqueIds) { + DeviceSession deviceSession = endpointSessions.get(uniqueId); + if (deviceSession != null) { + deviceSession.setLastUpdate(System.currentTimeMillis()); + return deviceSession; + } } } } else { - return endpointSessions.values().stream().findAny().orElse(null); + if (endpointSessions != null) { + DeviceSession deviceSession = endpointSessions.values().stream().findAny().orElse(null); + if (deviceSession != null) { + deviceSession.setLastUpdate(System.currentTimeMillis()); + } + return deviceSession; + } + return null; } Device device = deviceLookupService.lookup(uniqueIds); @@ -154,11 +166,14 @@ public class ConnectionManager implements BroadcastInterface { DeviceSession deviceSession = new DeviceSession( device.getId(), device.getUniqueId(), device.getModel(), protocol, channel, remoteAddress); - endpointSessions.put(device.getUniqueId(), deviceSession); + sessionsByEndpoint + .computeIfAbsent(connectionKey, k -> new ConcurrentHashMap<>()) + .put(device.getUniqueId(), deviceSession); sessionsByDeviceId.put(device.getId(), deviceSession); - if (oldSession == null) { - cacheManager.addDevice(device.getId(), connectionKey); + cacheManager.addDevice(device.getId(), connectionKey); + if (oldSession != null) { + cacheManager.removeDevice(device.getId(), oldSession.getConnectionKey()); } return deviceSession; @@ -256,19 +271,6 @@ public class ConnectionManager implements BroadcastInterface { device.setLastUpdate(time); } - Timeout timeout = timeouts.remove(deviceId); - if (timeout != null) { - timeout.cancel(); - } - - if (status.equals(Device.STATUS_ONLINE)) { - timeouts.put(deviceId, timer.newTimeout(timeout1 -> { - if (!timeout1.isCancelled()) { - deviceUnknown(deviceId); - } - }, deviceTimeout, TimeUnit.SECONDS)); - } - try { storage.updateObject(device, new Request( new Columns.Include("status", "lastUpdate"), @@ -293,7 +295,6 @@ public class ConnectionManager implements BroadcastInterface { if (local) { broadcastService.updateDevice(true, device); } else if (Device.STATUS_ONLINE.equals(device.getStatus())) { - timeouts.remove(device.getId()); removeDeviceSession(device.getId()); } for (long userId : deviceUsers.getOrDefault(device.getId(), Collections.emptySet())) {