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 --- .../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 +++ 6 files changed, 369 insertions(+), 295 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 (limited to 'src/main/java/org/traccar/session/cache') 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); + } + +} -- cgit v1.2.3