From c165968c2eb24b1c4a35dab39174b4df3576551c Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 17 Dec 2023 11:43:51 -0800 Subject: Improve cache update performance --- src/main/java/org/traccar/BaseProtocolDecoder.java | 3 +- .../java/org/traccar/api/BaseObjectResource.java | 10 +- .../traccar/api/resource/AttributeResource.java | 6 +- .../org/traccar/api/resource/DeviceResource.java | 2 +- .../traccar/api/resource/PermissionsResource.java | 8 +- .../org/traccar/api/resource/ServerResource.java | 19 +- .../org/traccar/api/resource/UserResource.java | 2 +- .../traccar/broadcast/BaseBroadcastService.java | 37 +- .../org/traccar/broadcast/BroadcastInterface.java | 16 +- .../broadcast/MulticastBroadcastService.java | 2 +- .../traccar/broadcast/RedisBroadcastService.java | 11 +- .../org/traccar/database/NotificationManager.java | 5 +- .../traccar/notificators/NotificatorFirebase.java | 6 +- .../traccar/notificators/NotificatorTraccar.java | 5 +- .../org/traccar/session/ConnectionManager.java | 9 +- .../java/org/traccar/session/cache/CacheGraph.java | 139 ++++++++ .../java/org/traccar/session/cache/CacheKey.java | 4 + .../org/traccar/session/cache/CacheManager.java | 384 ++++++++------------- .../java/org/traccar/session/cache/CacheNode.java | 40 +++ .../java/org/traccar/session/cache/CacheValue.java | 53 --- .../org/traccar/session/cache/WeakValueMap.java | 44 +++ .../events/MaintenanceEventHandlerTest.java | 17 +- 22 files changed, 443 insertions(+), 379 deletions(-) create mode 100644 src/main/java/org/traccar/session/cache/CacheGraph.java create mode 100644 src/main/java/org/traccar/session/cache/CacheNode.java delete mode 100644 src/main/java/org/traccar/session/cache/CacheValue.java create mode 100644 src/main/java/org/traccar/session/cache/WeakValueMap.java diff --git a/src/main/java/org/traccar/BaseProtocolDecoder.java b/src/main/java/org/traccar/BaseProtocolDecoder.java index 97762e8ca..4d4086c3c 100644 --- a/src/main/java/org/traccar/BaseProtocolDecoder.java +++ b/src/main/java/org/traccar/BaseProtocolDecoder.java @@ -29,7 +29,6 @@ import org.traccar.model.Position; import org.traccar.session.ConnectionManager; import org.traccar.session.DeviceSession; import org.traccar.session.cache.CacheManager; -import org.traccar.storage.StorageException; import jakarta.inject.Inject; import java.net.InetSocketAddress; @@ -137,7 +136,7 @@ public abstract class BaseProtocolDecoder extends ExtendedObjectDecoder { public DeviceSession getDeviceSession(Channel channel, SocketAddress remoteAddress, String... uniqueIds) { try { return connectionManager.getDeviceSession(protocol, channel, remoteAddress, uniqueIds); - } catch (StorageException e) { + } catch (Exception e) { throw new RuntimeException(e); } } diff --git a/src/main/java/org/traccar/api/BaseObjectResource.java b/src/main/java/org/traccar/api/BaseObjectResource.java index ebfa93ff0..2a801221b 100644 --- a/src/main/java/org/traccar/api/BaseObjectResource.java +++ b/src/main/java/org/traccar/api/BaseObjectResource.java @@ -67,7 +67,7 @@ public abstract class BaseObjectResource extends BaseResour } @POST - public Response add(T entity) throws StorageException { + public Response add(T entity) throws Exception { permissionsService.checkEdit(getUserId(), entity, true); entity.setId(storage.addObject(entity, new Request(new Columns.Exclude("id")))); @@ -85,7 +85,7 @@ public abstract class BaseObjectResource extends BaseResour @Path("{id}") @PUT - public Response update(T entity) throws StorageException { + public Response update(T entity) throws Exception { permissionsService.checkEdit(getUserId(), entity, false); permissionsService.checkPermission(baseClass, getUserId(), entity.getId()); @@ -111,7 +111,7 @@ public abstract class BaseObjectResource extends BaseResour new Condition.Equals("id", entity.getId()))); } } - cacheManager.updateOrInvalidate(true, entity, ObjectOperation.UPDATE); + cacheManager.invalidateObject(true, entity.getClass(), entity.getId(), ObjectOperation.UPDATE); LogAction.edit(getUserId(), entity); return Response.ok(entity).build(); @@ -119,12 +119,12 @@ public abstract class BaseObjectResource extends BaseResour @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws StorageException { + public Response remove(@PathParam("id") long id) throws Exception { permissionsService.checkEdit(getUserId(), baseClass, false); permissionsService.checkPermission(baseClass, getUserId(), id); storage.removeObject(baseClass, new Request(new Condition.Equals("id", id))); - cacheManager.invalidate(baseClass, id); + cacheManager.invalidateObject(true, baseClass, id, ObjectOperation.DELETE); LogAction.remove(getUserId(), baseClass, id); diff --git a/src/main/java/org/traccar/api/resource/AttributeResource.java b/src/main/java/org/traccar/api/resource/AttributeResource.java index 44f0ef452..52c4d6324 100644 --- a/src/main/java/org/traccar/api/resource/AttributeResource.java +++ b/src/main/java/org/traccar/api/resource/AttributeResource.java @@ -78,21 +78,21 @@ public class AttributeResource extends ExtendedObjectResource { } @POST - public Response add(Attribute entity) throws StorageException { + public Response add(Attribute entity) throws Exception { permissionsService.checkAdmin(getUserId()); return super.add(entity); } @Path("{id}") @PUT - public Response update(Attribute entity) throws StorageException { + public Response update(Attribute entity) throws Exception { permissionsService.checkAdmin(getUserId()); return super.update(entity); } @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws StorageException { + public Response remove(@PathParam("id") long id) throws Exception { permissionsService.checkAdmin(getUserId()); return super.remove(id); } diff --git a/src/main/java/org/traccar/api/resource/DeviceResource.java b/src/main/java/org/traccar/api/resource/DeviceResource.java index ebc40a9b1..217ccda65 100644 --- a/src/main/java/org/traccar/api/resource/DeviceResource.java +++ b/src/main/java/org/traccar/api/resource/DeviceResource.java @@ -128,7 +128,7 @@ public class DeviceResource extends BaseObjectResource { @Path("{id}/accumulators") @PUT - public Response updateAccumulators(DeviceAccumulators entity) throws StorageException { + public Response updateAccumulators(DeviceAccumulators entity) throws Exception { if (permissionsService.notAdmin(getUserId())) { permissionsService.checkManager(getUserId()); permissionsService.checkPermission(Device.class, getUserId(), entity.getDeviceId()); diff --git a/src/main/java/org/traccar/api/resource/PermissionsResource.java b/src/main/java/org/traccar/api/resource/PermissionsResource.java index 2a8ac62f7..9e2d21f2c 100644 --- a/src/main/java/org/traccar/api/resource/PermissionsResource.java +++ b/src/main/java/org/traccar/api/resource/PermissionsResource.java @@ -64,7 +64,7 @@ public class PermissionsResource extends BaseResource { @Path("bulk") @POST - public Response add(List> entities) throws StorageException, ClassNotFoundException { + public Response add(List> entities) throws Exception { permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap entity: entities) { @@ -84,13 +84,13 @@ public class PermissionsResource extends BaseResource { } @POST - public Response add(LinkedHashMap entity) throws StorageException, ClassNotFoundException { + public Response add(LinkedHashMap entity) throws Exception { return add(Collections.singletonList(entity)); } @DELETE @Path("bulk") - public Response remove(List> entities) throws StorageException, ClassNotFoundException { + public Response remove(List> entities) throws Exception { permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly); checkPermissionTypes(entities); for (LinkedHashMap entity: entities) { @@ -110,7 +110,7 @@ public class PermissionsResource extends BaseResource { } @DELETE - public Response remove(LinkedHashMap entity) throws StorageException, ClassNotFoundException { + public Response remove(LinkedHashMap entity) throws Exception { return remove(Collections.singletonList(entity)); } diff --git a/src/main/java/org/traccar/api/resource/ServerResource.java b/src/main/java/org/traccar/api/resource/ServerResource.java index 1ef2a6c33..66ecc74e1 100644 --- a/src/main/java/org/traccar/api/resource/ServerResource.java +++ b/src/main/java/org/traccar/api/resource/ServerResource.java @@ -107,14 +107,14 @@ public class ServerResource extends BaseResource { } @PUT - public Response update(Server entity) throws StorageException { + public Response update(Server server) throws Exception { permissionsService.checkAdmin(getUserId()); - storage.updateObject(entity, new Request( + storage.updateObject(server, new Request( new Columns.Exclude("id"), - new Condition.Equals("id", entity.getId()))); - cacheManager.updateOrInvalidate(true, entity, ObjectOperation.UPDATE); - LogAction.edit(getUserId(), entity); - return Response.ok(entity).build(); + new Condition.Equals("id", server.getId()))); + cacheManager.invalidateObject(true, Server.class, server.getId(), ObjectOperation.UPDATE); + LogAction.edit(getUserId(), server); + return Response.ok(server).build(); } @Path("geocode") @@ -157,4 +157,11 @@ public class ServerResource extends BaseResource { return Response.ok().build(); } + @Path("cache") + @GET + public String cache() throws StorageException { + permissionsService.checkAdmin(getUserId()); + return cacheManager.toString(); + } + } diff --git a/src/main/java/org/traccar/api/resource/UserResource.java b/src/main/java/org/traccar/api/resource/UserResource.java index 99537f912..47ea9b07c 100644 --- a/src/main/java/org/traccar/api/resource/UserResource.java +++ b/src/main/java/org/traccar/api/resource/UserResource.java @@ -126,7 +126,7 @@ public class UserResource extends BaseObjectResource { @Path("{id}") @DELETE - public Response remove(@PathParam("id") long id) throws StorageException { + public Response remove(@PathParam("id") long id) throws Exception { Response response = super.remove(id); if (getUserId() == id) { request.getSession().removeAttribute(SessionResource.USER_ID_KEY); diff --git a/src/main/java/org/traccar/broadcast/BaseBroadcastService.java b/src/main/java/org/traccar/broadcast/BaseBroadcastService.java index 1c4660320..01b212c60 100644 --- a/src/main/java/org/traccar/broadcast/BaseBroadcastService.java +++ b/src/main/java/org/traccar/broadcast/BaseBroadcastService.java @@ -69,10 +69,8 @@ public abstract class BaseBroadcastService implements BroadcastService { } @Override - public void invalidateObject( - boolean local, - Class clazz, long id, - ObjectOperation operation) { + public void invalidateObject( + boolean local, Class clazz, long id, ObjectOperation operation) { BroadcastMessage message = new BroadcastMessage(); var invalidateObject = new BroadcastMessage.InvalidateObject(); invalidateObject.setClazz(Permission.getKey(clazz)); @@ -83,11 +81,8 @@ public abstract class BaseBroadcastService implements BroadcastService { } @Override - public void invalidatePermission( - boolean local, - Class clazz1, long id1, - Class clazz2, long id2, - boolean link) { + public synchronized void invalidatePermission( + boolean local, Class clazz1, long id1, Class clazz2, long id2, boolean link) { BroadcastMessage message = new BroadcastMessage(); var invalidatePermission = new BroadcastMessage.InvalidatePermission(); invalidatePermission.setClazz1(Permission.getKey(clazz1)); @@ -101,7 +96,7 @@ public abstract class BaseBroadcastService implements BroadcastService { protected abstract void sendMessage(BroadcastMessage message); - protected void handleMessage(BroadcastMessage message) { + protected void handleMessage(BroadcastMessage message) throws Exception { if (message.getDevice() != null) { listeners.forEach(listener -> listener.updateDevice(false, message.getDevice())); } else if (message.getPosition() != null) { @@ -112,17 +107,21 @@ public abstract class BaseBroadcastService implements BroadcastService { listeners.forEach(listener -> listener.updateCommand(false, message.getCommandDeviceId())); } else if (message.getInvalidateObject() != null) { var invalidateObject = message.getInvalidateObject(); - listeners.forEach(listeners -> listeners.invalidateObject( - false, - Permission.getKeyClass(invalidateObject.getClazz()), invalidateObject.getId(), - invalidateObject.getOperation())); + for (BroadcastInterface listener : listeners) { + listener.invalidateObject( + false, + Permission.getKeyClass(invalidateObject.getClazz()), invalidateObject.getId(), + invalidateObject.getOperation()); + } } else if (message.getInvalidatePermission() != null) { var invalidatePermission = message.getInvalidatePermission(); - listeners.forEach(listener -> listener.invalidatePermission( - false, - Permission.getKeyClass(invalidatePermission.getClazz1()), invalidatePermission.getId1(), - Permission.getKeyClass(invalidatePermission.getClazz2()), invalidatePermission.getId2(), - invalidatePermission.getLink())); + for (BroadcastInterface listener : listeners) { + listener.invalidatePermission( + false, + Permission.getKeyClass(invalidatePermission.getClazz1()), invalidatePermission.getId1(), + Permission.getKeyClass(invalidatePermission.getClazz2()), invalidatePermission.getId2(), + invalidatePermission.getLink()); + } } } diff --git a/src/main/java/org/traccar/broadcast/BroadcastInterface.java b/src/main/java/org/traccar/broadcast/BroadcastInterface.java index 25fdf4d93..d0a491cd2 100644 --- a/src/main/java/org/traccar/broadcast/BroadcastInterface.java +++ b/src/main/java/org/traccar/broadcast/BroadcastInterface.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2022 - 2023 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. @@ -35,16 +35,12 @@ public interface BroadcastInterface { default void updateCommand(boolean local, long deviceId) { } - default void invalidateObject( - boolean local, - Class clazz, long id, - ObjectOperation operation) { + default void invalidateObject( + boolean local, Class clazz, long id, ObjectOperation operation) throws Exception { } - default void invalidatePermission( - boolean local, - Class clazz1, long id1, - Class clazz2, long id2, - boolean link) { + default void invalidatePermission( + boolean local, Class clazz1, long id1, Class clazz2, long id2, boolean link) throws Exception { } + } diff --git a/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java b/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java index 1c02b319b..793c6df36 100644 --- a/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java +++ b/src/main/java/org/traccar/broadcast/MulticastBroadcastService.java @@ -103,7 +103,7 @@ public class MulticastBroadcastService extends BaseBroadcastService { } publisherSocket = null; socket.leaveGroup(group, networkInterface); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } } diff --git a/src/main/java/org/traccar/broadcast/RedisBroadcastService.java b/src/main/java/org/traccar/broadcast/RedisBroadcastService.java index e87ad5e61..697c45a4a 100644 --- a/src/main/java/org/traccar/broadcast/RedisBroadcastService.java +++ b/src/main/java/org/traccar/broadcast/RedisBroadcastService.java @@ -38,7 +38,6 @@ public class RedisBroadcastService extends BaseBroadcastService { private final ExecutorService service = Executors.newSingleThreadExecutor(); - private final String url; private final String channel = "traccar"; private Jedis subscriber; @@ -48,7 +47,7 @@ public class RedisBroadcastService extends BaseBroadcastService { public RedisBroadcastService(Config config, ObjectMapper objectMapper) throws IOException { this.objectMapper = objectMapper; - url = config.getString(Keys.BROADCAST_ADDRESS); + String url = config.getString(Keys.BROADCAST_ADDRESS); try { subscriber = new Jedis(url); @@ -69,9 +68,7 @@ public class RedisBroadcastService extends BaseBroadcastService { try { String payload = id + ":" + objectMapper.writeValueAsString(message); publisher.publish(channel, payload); - } catch (IOException e) { - LOGGER.warn("Broadcast failed", e); - } catch (JedisConnectionException e) { + } catch (IOException | JedisConnectionException e) { LOGGER.warn("Broadcast failed", e); } } @@ -114,13 +111,11 @@ public class RedisBroadcastService extends BaseBroadcastService { if (messageChannel.equals(channel) && parts.length == 2 && !id.equals(parts[0])) { handleMessage(objectMapper.readValue(parts[1], BroadcastMessage.class)); } - } catch (IOException e) { + } catch (Exception e) { LOGGER.warn("Broadcast handleMessage failed", e); } } }, channel); - } catch (JedisConnectionException e) { - throw new RuntimeException(e); } catch (JedisException e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/traccar/database/NotificationManager.java b/src/main/java/org/traccar/database/NotificationManager.java index 79585d67a..45263ff3c 100644 --- a/src/main/java/org/traccar/database/NotificationManager.java +++ b/src/main/java/org/traccar/database/NotificationManager.java @@ -29,7 +29,6 @@ import org.traccar.model.Device; import org.traccar.model.Event; import org.traccar.model.Geofence; import org.traccar.model.Maintenance; -import org.traccar.model.Notification; import org.traccar.model.Position; import org.traccar.notification.MessageException; import org.traccar.notification.NotificatorManager; @@ -88,7 +87,7 @@ public class NotificationManager { return; } - var notifications = cacheManager.getDeviceObjects(event.getDeviceId(), Notification.class).stream() + var notifications = cacheManager.getDeviceNotifications(event.getDeviceId()) .filter(notification -> notification.getType().equals(event.getType())) .filter(notification -> { if (event.getType().equals(Event.TYPE_ALARM)) { @@ -162,7 +161,7 @@ public class NotificationManager { try { cacheManager.addDevice(event.getDeviceId()); updateEvent(event, position); - } catch (StorageException e) { + } catch (Exception e) { throw new RuntimeException(e); } finally { cacheManager.removeDevice(event.getDeviceId()); diff --git a/src/main/java/org/traccar/notificators/NotificatorFirebase.java b/src/main/java/org/traccar/notificators/NotificatorFirebase.java index a39683b2b..d75eb21a9 100644 --- a/src/main/java/org/traccar/notificators/NotificatorFirebase.java +++ b/src/main/java/org/traccar/notificators/NotificatorFirebase.java @@ -24,7 +24,6 @@ import com.google.firebase.messaging.AndroidNotification; import com.google.firebase.messaging.ApnsConfig; import com.google.firebase.messaging.Aps; import com.google.firebase.messaging.FirebaseMessaging; -import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.MessagingErrorCode; import com.google.firebase.messaging.MulticastMessage; import org.slf4j.Logger; @@ -40,7 +39,6 @@ import org.traccar.notification.MessageException; import org.traccar.notification.NotificationFormatter; import org.traccar.session.cache.CacheManager; import org.traccar.storage.Storage; -import org.traccar.storage.StorageException; import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; @@ -136,9 +134,9 @@ public class NotificatorFirebase implements Notificator { storage.updateObject(user, new Request( new Columns.Include("attributes"), new Condition.Equals("id", user.getId()))); - cacheManager.updateOrInvalidate(true, user, ObjectOperation.UPDATE); + cacheManager.invalidateObject(true, User.class, user.getId(), ObjectOperation.UPDATE); } - } catch (FirebaseMessagingException | StorageException e) { + } catch (Exception e) { LOGGER.warn("Firebase error", e); } } diff --git a/src/main/java/org/traccar/notificators/NotificatorTraccar.java b/src/main/java/org/traccar/notificators/NotificatorTraccar.java index 82e1584a5..717742a1e 100644 --- a/src/main/java/org/traccar/notificators/NotificatorTraccar.java +++ b/src/main/java/org/traccar/notificators/NotificatorTraccar.java @@ -27,7 +27,6 @@ import org.traccar.model.User; import org.traccar.notification.NotificationFormatter; import org.traccar.session.cache.CacheManager; import org.traccar.storage.Storage; -import org.traccar.storage.StorageException; import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; @@ -129,9 +128,9 @@ public class NotificatorTraccar implements Notificator { storage.updateObject(user, new Request( new Columns.Include("attributes"), new Condition.Equals("id", user.getId()))); - cacheManager.updateOrInvalidate(true, user, ObjectOperation.UPDATE); + cacheManager.invalidateObject(true, User.class, user.getId(), ObjectOperation.UPDATE); } - } catch (StorageException e) { + } catch (Exception e) { LOGGER.warn("Push error", e); } } diff --git a/src/main/java/org/traccar/session/ConnectionManager.java b/src/main/java/org/traccar/session/ConnectionManager.java index 0b13a5a72..3716fdf9a 100644 --- a/src/main/java/org/traccar/session/ConnectionManager.java +++ b/src/main/java/org/traccar/session/ConnectionManager.java @@ -102,7 +102,7 @@ public class ConnectionManager implements BroadcastInterface { public DeviceSession getDeviceSession( Protocol protocol, Channel channel, SocketAddress remoteAddress, - String... uniqueIds) throws StorageException { + String... uniqueIds) throws Exception { Endpoint endpoint = new Endpoint(channel, remoteAddress); Map endpointSessions = sessionsByEndpoint.getOrDefault( @@ -327,11 +327,8 @@ public class ConnectionManager implements BroadcastInterface { } @Override - public synchronized void invalidatePermission( - boolean local, - Class clazz1, long id1, - Class clazz2, long id2, - boolean link) { + public synchronized void invalidatePermission( + boolean local, Class clazz1, long id1, Class clazz2, long id2, boolean link) { if (link && clazz1.equals(User.class) && clazz2.equals(Device.class)) { if (listeners.containsKey(id1)) { userDevices.get(id1).add(id2); diff --git a/src/main/java/org/traccar/session/cache/CacheGraph.java b/src/main/java/org/traccar/session/cache/CacheGraph.java new file mode 100644 index 000000000..c99997288 --- /dev/null +++ b/src/main/java/org/traccar/session/cache/CacheGraph.java @@ -0,0 +1,139 @@ +/* + * Copyright 2023 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.session.cache; + +import org.traccar.model.BaseModel; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class CacheGraph { + + private final Map roots = new HashMap<>(); + private final WeakValueMap nodes = new WeakValueMap<>(); + + void addObject(BaseModel value) { + CacheKey key = new CacheKey(value); + CacheNode node = new CacheNode(value); + roots.put(key, node); + nodes.put(key, node); + } + + void removeObject(Class clazz, long id) { + CacheKey key = new CacheKey(clazz, id); + CacheNode node = nodes.remove(key); + if (node != null) { + node.getAllLinks(false).forEach(child -> child.getLinks(key.getClazz(), true).remove(node)); + } + roots.remove(key); + } + + @SuppressWarnings("unchecked") + T getObject(Class clazz, long id) { + CacheNode node = nodes.get(new CacheKey(clazz, id)); + return node != null ? (T) node.getValue() : null; + } + + Stream getObjects( + Class fromClass, long fromId, + Class clazz, Set> proxies, boolean forward) { + + CacheNode rootNode = nodes.get(new CacheKey(fromClass, fromId)); + if (rootNode != null) { + return getObjectStream(rootNode, clazz, proxies, forward); + } else { + return Stream.empty(); + } + } + + @SuppressWarnings("unchecked") + private Stream getObjectStream( + CacheNode rootNode, Class clazz, Set> proxies, boolean forward) { + + if (proxies.contains(clazz)) { + return Stream.empty(); + } + + var directSteam = rootNode.getLinks(clazz, forward).stream() + .map(node -> (T) node.getValue()); + + var proxyStream = proxies.stream() + .flatMap(proxyClass -> rootNode.getLinks(proxyClass, forward).stream() + .flatMap(node -> getObjectStream(node, clazz, proxies, forward))); + + return Stream.concat(directSteam, proxyStream); + } + + void updateObject(BaseModel value) { + CacheNode node = nodes.get(new CacheKey(value)); + if (node != null) { + node.setValue(value); + } + } + + boolean addLink( + Class fromClazz, long fromId, + BaseModel toValue) { + boolean stop = true; + CacheNode fromNode = nodes.get(new CacheKey(fromClazz, fromId)); + if (fromNode != null) { + CacheKey toKey = new CacheKey(toValue); + CacheNode toNode = nodes.get(toKey); + if (toNode == null) { + stop = false; + toNode = new CacheNode(toValue); + nodes.put(toKey, toNode); + } + fromNode.getLinks(toValue.getClass(), true).add(toNode); + toNode.getLinks(fromClazz, false).add(fromNode); + } + return stop; + } + + void removeLink( + Class fromClazz, long fromId, + Class toClazz, long toId) { + CacheNode fromNode = nodes.get(new CacheKey(fromClazz, fromId)); + if (fromNode != null) { + CacheNode toNode = nodes.get(new CacheKey(toClazz, toId)); + if (toNode != null) { + fromNode.getLinks(toClazz, true).remove(toNode); + toNode.getLinks(fromClazz, false).remove(fromNode); + } + } + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + for (CacheNode node : roots.values()) { + printNode(stringBuilder, node, ""); + } + return stringBuilder.toString().trim(); + } + + private void printNode(StringBuilder stringBuilder, CacheNode node, String indentation) { + stringBuilder + .append('\n') + .append(indentation) + .append(node.getValue().getClass().getSimpleName()) + .append('(').append(node.getValue().getId()).append(')'); + node.getAllLinks(true).forEach(child -> printNode(stringBuilder, child, indentation + " ")); + } + +} diff --git a/src/main/java/org/traccar/session/cache/CacheKey.java b/src/main/java/org/traccar/session/cache/CacheKey.java index 23145e34b..f27d5fbf5 100644 --- a/src/main/java/org/traccar/session/cache/CacheKey.java +++ b/src/main/java/org/traccar/session/cache/CacheKey.java @@ -33,6 +33,10 @@ class CacheKey { this.id = id; } + public Class getClazz() { + return clazz; + } + public boolean classIs(Class clazz) { return clazz.equals(this.clazz); } diff --git a/src/main/java/org/traccar/session/cache/CacheManager.java b/src/main/java/org/traccar/session/cache/CacheManager.java index dc9c86ef3..918c97c66 100644 --- a/src/main/java/org/traccar/session/cache/CacheManager.java +++ b/src/main/java/org/traccar/session/cache/CacheManager.java @@ -15,11 +15,10 @@ */ package org.traccar.session.cache; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; import org.traccar.broadcast.BroadcastInterface; import org.traccar.broadcast.BroadcastService; -import org.traccar.model.ObjectOperation; import org.traccar.config.Config; import org.traccar.model.Attribute; import org.traccar.model.BaseModel; @@ -31,6 +30,8 @@ import org.traccar.model.Group; import org.traccar.model.GroupedModel; import org.traccar.model.Maintenance; import org.traccar.model.Notification; +import org.traccar.model.ObjectOperation; +import org.traccar.model.Permission; import org.traccar.model.Position; import org.traccar.model.Schedulable; import org.traccar.model.Server; @@ -41,30 +42,20 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; +import java.util.stream.Stream; @Singleton public class CacheManager implements BroadcastInterface { - private static final Logger LOGGER = LoggerFactory.getLogger(CacheManager.class); - private static final int GROUP_DEPTH_LIMIT = 3; - private static final Collection> CLASSES = Arrays.asList( - Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class); + private static final Set> GROUPED_CLASSES = + Set.of(Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class); private final Config config; private final Storage storage; @@ -72,24 +63,26 @@ public class CacheManager implements BroadcastInterface { private final ReadWriteLock lock = new ReentrantReadWriteLock(); - private final Map deviceCache = new HashMap<>(); - private final Map deviceReferences = new HashMap<>(); - private final Map, Set>> deviceLinks = new HashMap<>(); - private final Map devicePositions = new HashMap<>(); + private final CacheGraph graph = new CacheGraph(); private Server server; - private final Map> notificationUsers = new HashMap<>(); + private final Map devicePositions = new HashMap<>(); + private final Map deviceReferences = new HashMap<>(); @Inject public CacheManager(Config config, Storage storage, BroadcastService broadcastService) throws StorageException { this.config = config; this.storage = storage; this.broadcastService = broadcastService; - invalidateServer(); - invalidateUsers(); + server = storage.getObject(Server.class, new Request(new Columns.All())); broadcastService.registerListener(this); } + @Override + public String toString() { + return graph.toString(); + } + public Config getConfig() { return config; } @@ -97,29 +90,17 @@ public class CacheManager implements BroadcastInterface { public T getObject(Class clazz, long id) { try { lock.readLock().lock(); - var cacheValue = deviceCache.get(new CacheKey(clazz, id)); - return cacheValue != null ? cacheValue.getValue() : null; + return graph.getObject(clazz, id); } finally { lock.readLock().unlock(); } } - public List getDeviceObjects(long deviceId, Class clazz) { + public Set getDeviceObjects(long deviceId, Class clazz) { try { lock.readLock().lock(); - var links = deviceLinks.get(deviceId); - if (links != null) { - return links.getOrDefault(clazz, new LinkedHashSet<>()).stream() - .map(id -> { - var cacheValue = deviceCache.get(new CacheKey(clazz, id)); - return cacheValue != null ? cacheValue.getValue() : null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } else { - LOGGER.warn("Device {} cache missing", deviceId); - return Collections.emptyList(); - } + return graph.getObjects(Device.class, deviceId, clazz, Set.of(Group.class), true) + .collect(Collectors.toUnmodifiableSet()); } finally { lock.readLock().unlock(); } @@ -143,30 +124,40 @@ public class CacheManager implements BroadcastInterface { } } - public List getNotificationUsers(long notificationId, long deviceId) { + public Set getNotificationUsers(long notificationId, long deviceId) { + try { + lock.readLock().lock(); + Set deviceUsers = getDeviceObjects(deviceId, User.class); + return graph.getObjects(Notification.class, notificationId, User.class, Set.of(), false) + .filter(deviceUsers::contains) + .collect(Collectors.toUnmodifiableSet()); + } finally { + lock.readLock().unlock(); + } + } + + public Stream getDeviceNotifications(long deviceId) { try { lock.readLock().lock(); - var users = deviceLinks.get(deviceId).get(User.class).stream() + var direct = graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class), true) + .map(BaseModel::getId) .collect(Collectors.toUnmodifiableSet()); - return notificationUsers.getOrDefault(notificationId, new LinkedList<>()).stream() - .filter(user -> users.contains(user.getId())) - .collect(Collectors.toUnmodifiableList()); + return graph.getObjects(Device.class, deviceId, Notification.class, Set.of(Group.class, User.class), true) + .filter(notification -> notification.getAlways() || direct.contains(notification.getId())); } finally { lock.readLock().unlock(); } } - public void addDevice(long deviceId) throws StorageException { + public void addDevice(long deviceId) throws Exception { try { lock.writeLock().lock(); - Integer references = deviceReferences.get(deviceId); - if (references != null) { - references += 1; - } else { - unsafeAddDevice(deviceId); - references = 1; + if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).getAndIncrement() <= 0) { + Device device = storage.getObject(Device.class, new Request( + new Columns.All(), new Condition.Equals("id", deviceId))); + graph.addObject(device); + initializeCache(device); } - deviceReferences.put(deviceId, references); } finally { lock.writeLock().unlock(); } @@ -175,15 +166,10 @@ public class CacheManager implements BroadcastInterface { public void removeDevice(long deviceId) { try { lock.writeLock().lock(); - Integer references = deviceReferences.get(deviceId); - if (references != null) { - references -= 1; - if (references <= 0) { - unsafeRemoveDevice(deviceId); - deviceReferences.remove(deviceId); - } else { - deviceReferences.put(deviceId, references); - } + if (deviceReferences.computeIfAbsent(deviceId, k -> new AtomicInteger()).incrementAndGet() <= 0) { + graph.removeObject(Device.class, deviceId); + devicePositions.remove(deviceId); + deviceReferences.remove(deviceId); } } finally { lock.writeLock().unlock(); @@ -193,7 +179,7 @@ public class CacheManager implements BroadcastInterface { public void updatePosition(Position position) { try { lock.writeLock().lock(); - if (deviceLinks.containsKey(position.getDeviceId())) { + if (deviceReferences.containsKey(position.getDeviceId())) { devicePositions.put(position.getDeviceId(), position); } } finally { @@ -202,226 +188,140 @@ public class CacheManager implements BroadcastInterface { } @Override - public void invalidateObject( - boolean local, - Class clazz, long id, - ObjectOperation operation) { - try { - var object = storage.getObject(clazz, new Request( - new Columns.All(), new Condition.Equals("id", id))); - if (object != null) { - updateOrInvalidate(local, object, operation); - } else { - invalidate(clazz, id); - } - } catch (StorageException e) { - throw new RuntimeException(e); - } - } - - public void updateOrInvalidate( - boolean local, T object, ObjectOperation operation) throws StorageException { + public void invalidateObject( + boolean local, Class clazz, long id, ObjectOperation operation) throws Exception { if (local) { - broadcastService.invalidateObject(true, object.getClass(), object.getId(), operation); + broadcastService.invalidateObject(true, clazz, id, operation); } - if (object instanceof Server) { - invalidateServer(); + if (operation == ObjectOperation.DELETE) { + graph.removeObject(clazz, id); + } + if (operation != ObjectOperation.UPDATE) { return; } - if (object instanceof User) { - invalidateUsers(); + + if (clazz.equals(Server.class)) { + server = storage.getObject(Server.class, new Request(new Columns.All())); return; } - boolean invalidate = false; - var before = getObject(object.getClass(), object.getId()); + var after = storage.getObject(clazz, new Request(new Columns.All(), new Condition.Equals("id", id))); + if (after == null) { + return; + } + var before = getObject(after.getClass(), after.getId()); if (before == null) { return; - } else if (object instanceof GroupedModel) { - if (((GroupedModel) before).getGroupId() != ((GroupedModel) object).getGroupId()) { - invalidate = true; - } - } else if (object instanceof Schedulable) { - if (((Schedulable) before).getCalendarId() != ((Schedulable) object).getCalendarId()) { - invalidate = true; - } } - if (invalidate) { - invalidate(object.getClass(), object.getId()); - } else { - try { - lock.writeLock().lock(); - deviceCache.get(new CacheKey(object.getClass(), object.getId())).setValue(object); - } finally { - lock.writeLock().unlock(); + + if (after instanceof GroupedModel) { + long beforeGroupId = ((GroupedModel) before).getGroupId(); + long afterGroupId = ((GroupedModel) after).getGroupId(); + if (beforeGroupId != afterGroupId) { + if (beforeGroupId > 0) { + invalidatePermission(clazz, id, Group.class, beforeGroupId, false); + } + if (afterGroupId > 0) { + invalidatePermission(clazz, id, Group.class, afterGroupId, true); + } } + } else if (after instanceof Schedulable) { + long beforeCalendarId = ((Schedulable) before).getCalendarId(); + long afterCalendarId = ((Schedulable) after).getCalendarId(); + if (beforeCalendarId != afterCalendarId) { + if (beforeCalendarId > 0) { + invalidatePermission(clazz, id, Calendar.class, beforeCalendarId, false); + } + if (afterCalendarId > 0) { + invalidatePermission(clazz, id, Calendar.class, afterCalendarId, true); + } + } + // TODO handle notification always change } - } - public void invalidate(Class clazz, long id) throws StorageException { - invalidate(new CacheKey(clazz, id)); + graph.updateObject(after); } @Override - public void invalidatePermission( - boolean local, - Class clazz1, long id1, - Class clazz2, long id2, - boolean link) { + public void invalidatePermission( + boolean local, Class clazz1, long id1, Class clazz2, long id2, boolean link) throws Exception { if (local) { broadcastService.invalidatePermission(true, clazz1, id1, clazz2, id2, link); } - try { - invalidate(new CacheKey(clazz1, id1), new CacheKey(clazz2, id2)); - } catch (StorageException e) { - throw new RuntimeException(e); + if (clazz1.equals(User.class) && GroupedModel.class.isAssignableFrom(clazz2)) { + invalidatePermission(clazz2, id2, clazz1, id1, link); + } else { + invalidatePermission(clazz1, id1, clazz2, id2, link); } } - private void invalidateServer() throws StorageException { - server = storage.getObject(Server.class, new Request(new Columns.All())); - } + private void invalidatePermission( + Class fromClass, long fromId, Class toClass, long toId, boolean link) throws Exception { - private void invalidateUsers() throws StorageException { - notificationUsers.clear(); - Map users = new HashMap<>(); - storage.getObjects(User.class, new Request(new Columns.All())) - .forEach(user -> users.put(user.getId(), user)); - storage.getPermissions(User.class, Notification.class).forEach(permission -> { - long notificationId = permission.getPropertyId(); - var user = users.get(permission.getOwnerId()); - notificationUsers.computeIfAbsent(notificationId, k -> new LinkedList<>()).add(user); - }); - } + boolean groupLink = GroupedModel.class.isAssignableFrom(fromClass) && toClass.equals(Group.class); + boolean calendarLink = Schedulable.class.isAssignableFrom(fromClass) && toClass.equals(Calendar.class); + boolean userLink = fromClass.equals(User.class) && toClass.equals(Notification.class); - private void addObject(long deviceId, BaseModel object) { - deviceCache.computeIfAbsent(new CacheKey(object), k -> new CacheValue(object)).retain(deviceId); - } + boolean groupedLinks = GroupedModel.class.isAssignableFrom(fromClass) + && (GROUPED_CLASSES.contains(toClass) || toClass.equals(User.class)); - private void unsafeAddDevice(long deviceId) throws StorageException { - Map, Set> links = new HashMap<>(); - - Device device = storage.getObject(Device.class, new Request( - new Columns.All(), new Condition.Equals("id", deviceId))); - if (device != null) { - addObject(deviceId, device); - if (device.getCalendarId() > 0) { - var calendar = storage.getObject(Calendar.class, new Request( - new Columns.All(), new Condition.Equals("id", device.getCalendarId()))); - links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId()); - addObject(deviceId, calendar); - } + if (!groupLink && !calendarLink && !userLink && !groupedLinks) { + return; + } - int groupDepth = 0; - long groupId = device.getGroupId(); - while (groupDepth < GROUP_DEPTH_LIMIT && groupId > 0) { - Group group = storage.getObject(Group.class, new Request( - new Columns.All(), new Condition.Equals("id", groupId))); - links.computeIfAbsent(Group.class, k -> new LinkedHashSet<>()).add(group.getId()); - addObject(deviceId, group); - groupId = group.getGroupId(); - groupDepth += 1; + if (link) { + BaseModel object = storage.getObject(toClass, new Request( + new Columns.All(), new Condition.Equals("id", toId))); + if (!graph.addLink(fromClass, fromId, object)) { + initializeCache(object); } + } else { + graph.removeLink(fromClass, fromId, toClass, toId); + } + } - for (Class clazz : CLASSES) { - var objects = storage.getObjects(clazz, new Request( - new Columns.All(), new Condition.Permission(Device.class, deviceId, clazz))); - links.put(clazz, objects.stream().map(BaseModel::getId).collect(Collectors.toSet())); - for (var object : objects) { - addObject(deviceId, object); - if (object instanceof Schedulable) { - var scheduled = (Schedulable) object; - if (scheduled.getCalendarId() > 0) { - var calendar = storage.getObject(Calendar.class, new Request( - new Columns.All(), new Condition.Equals("id", scheduled.getCalendarId()))); - links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId()); - addObject(deviceId, calendar); - } - } + private void initializeCache(BaseModel object) throws Exception { + if (object instanceof User) { + for (Permission permission : storage.getPermissions(User.class, Notification.class)) { + if (permission.getOwnerId() == object.getId()) { + invalidatePermission( + permission.getOwnerClass(), permission.getOwnerId(), + permission.getPropertyClass(), permission.getPropertyId(), true); } } + } else { + if (object instanceof GroupedModel) { + long groupId = ((GroupedModel) object).getGroupId(); + if (groupId > 0) { + invalidatePermission(object.getClass(), object.getId(), Group.class, groupId, true); + } - var users = storage.getObjects(User.class, new Request( - new Columns.All(), new Condition.Permission(User.class, Device.class, deviceId))); - links.put(User.class, users.stream().map(BaseModel::getId).collect(Collectors.toSet())); - for (var user : users) { - addObject(deviceId, user); - var notifications = storage.getObjects(Notification.class, new Request( - new Columns.All(), - new Condition.Permission(User.class, user.getId(), Notification.class))).stream() - .filter(Notification::getAlways) - .collect(Collectors.toList()); - for (var notification : notifications) { - links.computeIfAbsent(Notification.class, k -> new LinkedHashSet<>()).add(notification.getId()); - addObject(deviceId, notification); - if (notification.getCalendarId() > 0) { - var calendar = storage.getObject(Calendar.class, new Request( - new Columns.All(), new Condition.Equals("id", notification.getCalendarId()))); - links.computeIfAbsent(Calendar.class, k -> new LinkedHashSet<>()).add(calendar.getId()); - addObject(deviceId, calendar); + for (Permission permission : storage.getPermissions(User.class, object.getClass())) { + if (permission.getPropertyId() == object.getId()) { + invalidatePermission( + object.getClass(), object.getId(), User.class, permission.getOwnerId(), true); } } - } - - deviceLinks.put(deviceId, links); - if (device.getPositionId() > 0) { - devicePositions.put(deviceId, storage.getObject(Position.class, new Request( - new Columns.All(), new Condition.Equals("id", device.getPositionId())))); + for (Class clazz : GROUPED_CLASSES) { + for (Permission permission : storage.getPermissions(object.getClass(), clazz)) { + if (permission.getOwnerId() == object.getId()) { + invalidatePermission( + object.getClass(), object.getId(), clazz, permission.getPropertyId(), true); + } + } + } } - } - } - private void unsafeRemoveDevice(long deviceId) { - deviceCache.remove(new CacheKey(Device.class, deviceId)); - deviceLinks.remove(deviceId).forEach((clazz, ids) -> ids.forEach(id -> { - var key = new CacheKey(clazz, id); - deviceCache.computeIfPresent(key, (k, value) -> { - value.release(deviceId); - return value.getReferences().isEmpty() ? null : value; - }); - })); - devicePositions.remove(deviceId); - } - - private void invalidate(CacheKey... keys) throws StorageException { - try { - lock.writeLock().lock(); - unsafeInvalidate(keys); - } finally { - lock.writeLock().unlock(); - } - } - - private void unsafeInvalidate(CacheKey[] keys) throws StorageException { - boolean invalidateServer = false; - boolean invalidateUsers = false; - Set linkedDevices = new HashSet<>(); - for (var key : keys) { - if (key.classIs(Server.class)) { - invalidateServer = true; - } else { - if (key.classIs(User.class) || key.classIs(Notification.class)) { - invalidateUsers = true; + if (object instanceof Schedulable) { + long calendarId = ((Schedulable) object).getCalendarId(); + if (calendarId > 0) { + invalidatePermission(object.getClass(), object.getId(), Calendar.class, calendarId, true); } - deviceCache.computeIfPresent(key, (k, value) -> { - linkedDevices.addAll(value.getReferences()); - return value; - }); } } - for (long deviceId : linkedDevices) { - unsafeRemoveDevice(deviceId); - unsafeAddDevice(deviceId); - } - if (invalidateServer) { - invalidateServer(); - } - if (invalidateUsers) { - invalidateUsers(); - } } } diff --git a/src/main/java/org/traccar/session/cache/CacheNode.java b/src/main/java/org/traccar/session/cache/CacheNode.java new file mode 100644 index 000000000..7b584f81a --- /dev/null +++ b/src/main/java/org/traccar/session/cache/CacheNode.java @@ -0,0 +1,40 @@ +package org.traccar.session.cache; + +import org.traccar.model.BaseModel; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class CacheNode { + + private BaseModel value; + + private final Map, Set> links = new HashMap<>(); + private final Map, Set> backlinks = new HashMap<>(); + + public CacheNode(BaseModel value) { + this.value = value; + } + + public BaseModel getValue() { + return value; + } + + public void setValue(BaseModel value) { + this.value = value; + } + + public Set getLinks(Class clazz, boolean forward) { + var map = forward ? links : backlinks; + return map.computeIfAbsent(clazz, k -> new HashSet<>()); + } + + public Stream getAllLinks(boolean forward) { + var map = forward ? links : backlinks; + return map.values().stream().flatMap(Set::stream); + } + +} diff --git a/src/main/java/org/traccar/session/cache/CacheValue.java b/src/main/java/org/traccar/session/cache/CacheValue.java deleted file mode 100644 index 1f0383ce5..000000000 --- a/src/main/java/org/traccar/session/cache/CacheValue.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2022 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.session.cache; - -import org.traccar.model.BaseModel; - -import java.util.HashSet; -import java.util.Set; - -class CacheValue { - - private BaseModel value; - private final Set references = new HashSet<>(); - - CacheValue(BaseModel value) { - this.value = value; - } - - public void retain(long deviceId) { - references.add(deviceId); - } - - public void release(long deviceId) { - references.remove(deviceId); - } - - @SuppressWarnings("unchecked") - public T getValue() { - return (T) value; - } - - public void setValue(BaseModel value) { - this.value = value; - } - - public Set getReferences() { - return references; - } - -} diff --git a/src/main/java/org/traccar/session/cache/WeakValueMap.java b/src/main/java/org/traccar/session/cache/WeakValueMap.java new file mode 100644 index 000000000..8323e2c30 --- /dev/null +++ b/src/main/java/org/traccar/session/cache/WeakValueMap.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.session.cache; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +public class WeakValueMap { + + private final Map> map = new HashMap<>(); + + public void put(K key, V value) { + map.put(key, new WeakReference<>(value)); + } + + public V get(K key) { + WeakReference weakReference = map.get(key); + return (weakReference != null) ? weakReference.get() : null; + } + + public V remove(K key) { + WeakReference weakReference = map.remove(key); + return (weakReference != null) ? weakReference.get() : null; + } + + private void clean() { + map.entrySet().removeIf(entry -> entry.getValue().get() == null); + } + +} diff --git a/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java b/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java index 5320be926..661336d76 100644 --- a/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java +++ b/src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java @@ -6,14 +6,15 @@ import org.traccar.model.Maintenance; import org.traccar.model.Position; import org.traccar.session.cache.CacheManager; -import java.util.Arrays; import java.util.Date; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.anyLong; public class MaintenanceEventHandlerTest extends BaseTest { @@ -29,7 +30,7 @@ public class MaintenanceEventHandlerTest extends BaseTest { var maintenance = mock(Maintenance.class); when(maintenance.getType()).thenReturn(Position.KEY_TOTAL_DISTANCE); - var maintenances = Arrays.asList(maintenance); + var maintenances = Set.of(maintenance); var cacheManager = mock(CacheManager.class); when(cacheManager.getDeviceObjects(anyLong(), eq(Maintenance.class))).thenReturn(maintenances); @@ -48,12 +49,12 @@ public class MaintenanceEventHandlerTest extends BaseTest { assertTrue(eventHandler.analyzePosition(position).isEmpty()); lastPosition.set(Position.KEY_TOTAL_DISTANCE, 9999); - position.set(Position.KEY_TOTAL_DISTANCE, 10001); - assertTrue(eventHandler.analyzePosition(position).size() == 1); + position.set(Position.KEY_TOTAL_DISTANCE, 10001); + assertEquals(1, eventHandler.analyzePosition(position).size()); lastPosition.set(Position.KEY_TOTAL_DISTANCE, 11999); - position.set(Position.KEY_TOTAL_DISTANCE, 12001); - assertTrue(eventHandler.analyzePosition(position).size() == 1); + position.set(Position.KEY_TOTAL_DISTANCE, 12001); + assertEquals(1, eventHandler.analyzePosition(position).size()); } -- cgit v1.2.3