aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2023-12-17 11:43:51 -0800
committerAnton Tananaev <anton@traccar.org>2023-12-17 11:43:51 -0800
commitc165968c2eb24b1c4a35dab39174b4df3576551c (patch)
tree6dae35ddec3feb14151e5c711f4d0f5865cbc64e
parent4dc2e86ae5e403143b3c4e2c1a3bd671dfeaf5f8 (diff)
downloadtrackermap-server-c165968c2eb24b1c4a35dab39174b4df3576551c.tar.gz
trackermap-server-c165968c2eb24b1c4a35dab39174b4df3576551c.tar.bz2
trackermap-server-c165968c2eb24b1c4a35dab39174b4df3576551c.zip
Improve cache update performance
-rw-r--r--src/main/java/org/traccar/BaseProtocolDecoder.java3
-rw-r--r--src/main/java/org/traccar/api/BaseObjectResource.java10
-rw-r--r--src/main/java/org/traccar/api/resource/AttributeResource.java6
-rw-r--r--src/main/java/org/traccar/api/resource/DeviceResource.java2
-rw-r--r--src/main/java/org/traccar/api/resource/PermissionsResource.java8
-rw-r--r--src/main/java/org/traccar/api/resource/ServerResource.java19
-rw-r--r--src/main/java/org/traccar/api/resource/UserResource.java2
-rw-r--r--src/main/java/org/traccar/broadcast/BaseBroadcastService.java37
-rw-r--r--src/main/java/org/traccar/broadcast/BroadcastInterface.java16
-rw-r--r--src/main/java/org/traccar/broadcast/MulticastBroadcastService.java2
-rw-r--r--src/main/java/org/traccar/broadcast/RedisBroadcastService.java11
-rw-r--r--src/main/java/org/traccar/database/NotificationManager.java5
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorFirebase.java6
-rw-r--r--src/main/java/org/traccar/notificators/NotificatorTraccar.java5
-rw-r--r--src/main/java/org/traccar/session/ConnectionManager.java9
-rw-r--r--src/main/java/org/traccar/session/cache/CacheGraph.java139
-rw-r--r--src/main/java/org/traccar/session/cache/CacheKey.java4
-rw-r--r--src/main/java/org/traccar/session/cache/CacheManager.java384
-rw-r--r--src/main/java/org/traccar/session/cache/CacheNode.java40
-rw-r--r--src/main/java/org/traccar/session/cache/CacheValue.java53
-rw-r--r--src/main/java/org/traccar/session/cache/WeakValueMap.java44
-rw-r--r--src/test/java/org/traccar/handler/events/MaintenanceEventHandlerTest.java17
22 files changed, 443 insertions, 379 deletions
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<T extends BaseModel> 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<T extends BaseModel> 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<T extends BaseModel> 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<T extends BaseModel> 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<Attribute> {
}
@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<Device> {
@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<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException {
+ public Response add(List<LinkedHashMap<String, Long>> entities) throws Exception {
permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly);
checkPermissionTypes(entities);
for (LinkedHashMap<String, Long> entity: entities) {
@@ -84,13 +84,13 @@ public class PermissionsResource extends BaseResource {
}
@POST
- public Response add(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException {
+ public Response add(LinkedHashMap<String, Long> entity) throws Exception {
return add(Collections.singletonList(entity));
}
@DELETE
@Path("bulk")
- public Response remove(List<LinkedHashMap<String, Long>> entities) throws StorageException, ClassNotFoundException {
+ public Response remove(List<LinkedHashMap<String, Long>> entities) throws Exception {
permissionsService.checkRestriction(getUserId(), UserRestrictions::getReadonly);
checkPermissionTypes(entities);
for (LinkedHashMap<String, Long> entity: entities) {
@@ -110,7 +110,7 @@ public class PermissionsResource extends BaseResource {
}
@DELETE
- public Response remove(LinkedHashMap<String, Long> entity) throws StorageException, ClassNotFoundException {
+ public Response remove(LinkedHashMap<String, Long> 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<User> {
@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<? extends BaseModel> clazz, long id,
- ObjectOperation operation) {
+ public <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> 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<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public synchronized <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> 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<? extends BaseModel> clazz, long id,
- ObjectOperation operation) {
+ default <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> clazz, long id, ObjectOperation operation) throws Exception {
}
- default void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ default <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> 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<String, DeviceSession> endpointSessions = sessionsByEndpoint.getOrDefault(
@@ -327,11 +327,8 @@ public class ConnectionManager implements BroadcastInterface {
}
@Override
- public synchronized void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public synchronized <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> 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<CacheKey, CacheNode> roots = new HashMap<>();
+ private final WeakValueMap<CacheKey, CacheNode> 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<? extends BaseModel> 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 extends BaseModel> T getObject(Class<T> clazz, long id) {
+ CacheNode node = nodes.get(new CacheKey(clazz, id));
+ return node != null ? (T) node.getValue() : null;
+ }
+
+ <T extends BaseModel> Stream<T> getObjects(
+ Class<? extends BaseModel> fromClass, long fromId,
+ Class<T> clazz, Set<Class<? extends BaseModel>> 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 <T extends BaseModel> Stream<T> getObjectStream(
+ CacheNode rootNode, Class<T> clazz, Set<Class<? extends BaseModel>> 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<? extends BaseModel> 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<? extends BaseModel> fromClazz, long fromId,
+ Class<? extends BaseModel> 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<? extends BaseModel> getClazz() {
+ return clazz;
+ }
+
public boolean classIs(Class<? extends BaseModel> 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<Class<? extends BaseModel>> CLASSES = Arrays.asList(
- Attribute.class, Driver.class, Geofence.class, Maintenance.class, Notification.class);
+ private static final Set<Class<? extends BaseModel>> 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<CacheKey, CacheValue> deviceCache = new HashMap<>();
- private final Map<Long, Integer> deviceReferences = new HashMap<>();
- private final Map<Long, Map<Class<? extends BaseModel>, Set<Long>>> deviceLinks = new HashMap<>();
- private final Map<Long, Position> devicePositions = new HashMap<>();
+ private final CacheGraph graph = new CacheGraph();
private Server server;
- private final Map<Long, List<User>> notificationUsers = new HashMap<>();
+ private final Map<Long, Position> devicePositions = new HashMap<>();
+ private final Map<Long, AtomicInteger> 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 extends BaseModel> T getObject(Class<T> 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 <T extends BaseModel> List<T> getDeviceObjects(long deviceId, Class<T> clazz) {
+ public <T extends BaseModel> Set<T> getDeviceObjects(long deviceId, Class<T> 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.<T>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<User> getNotificationUsers(long notificationId, long deviceId) {
+ public Set<User> getNotificationUsers(long notificationId, long deviceId) {
+ try {
+ lock.readLock().lock();
+ Set<User> 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<Notification> 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<? extends BaseModel> 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 <T extends BaseModel> void updateOrInvalidate(
- boolean local, T object, ObjectOperation operation) throws StorageException {
+ public <T extends BaseModel> void invalidateObject(
+ boolean local, Class<T> 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 <T extends BaseModel> void invalidate(Class<T> clazz, long id) throws StorageException {
- invalidate(new CacheKey(clazz, id));
+ graph.updateObject(after);
}
@Override
- public void invalidatePermission(
- boolean local,
- Class<? extends BaseModel> clazz1, long id1,
- Class<? extends BaseModel> clazz2, long id2,
- boolean link) {
+ public <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ boolean local, Class<T1> clazz1, long id1, Class<T2> 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 <T1 extends BaseModel, T2 extends BaseModel> void invalidatePermission(
+ Class<T1> fromClass, long fromId, Class<T2> toClass, long toId, boolean link) throws Exception {
- private void invalidateUsers() throws StorageException {
- notificationUsers.clear();
- Map<Long, User> 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<Class<? extends BaseModel>, Set<Long>> 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<? extends BaseModel> 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<? extends BaseModel> 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<Long> 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<Class<? extends BaseModel>, Set<CacheNode>> links = new HashMap<>();
+ private final Map<Class<? extends BaseModel>, Set<CacheNode>> 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<CacheNode> getLinks(Class<? extends BaseModel> clazz, boolean forward) {
+ var map = forward ? links : backlinks;
+ return map.computeIfAbsent(clazz, k -> new HashSet<>());
+ }
+
+ public Stream<CacheNode> 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<Long> 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 extends BaseModel> T getValue() {
- return (T) value;
- }
-
- public void setValue(BaseModel value) {
- this.value = value;
- }
-
- public Set<Long> 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<K, V> {
+
+ private final Map<K, WeakReference<V>> map = new HashMap<>();
+
+ public void put(K key, V value) {
+ map.put(key, new WeakReference<>(value));
+ }
+
+ public V get(K key) {
+ WeakReference<V> weakReference = map.get(key);
+ return (weakReference != null) ? weakReference.get() : null;
+ }
+
+ public V remove(K key) {
+ WeakReference<V> 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());
}