aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-06-01 19:07:59 -0700
committerAnton Tananaev <anton@traccar.org>2022-06-01 19:07:59 -0700
commit5032f99c6462ca97f3629abe1faf2f50cc4977fe (patch)
tree8571f7fdbbc94018843f6f66f14d85272912e8ab
parent45b889838238e232295d332b4a6ae81fb112a806 (diff)
downloadtrackermap-server-5032f99c6462ca97f3629abe1faf2f50cc4977fe.tar.gz
trackermap-server-5032f99c6462ca97f3629abe1faf2f50cc4977fe.tar.bz2
trackermap-server-5032f99c6462ca97f3629abe1faf2f50cc4977fe.zip
New caching system
-rw-r--r--src/main/java/org/traccar/session/cache/CacheKey.java57
-rw-r--r--src/main/java/org/traccar/session/cache/CacheManager.java214
-rw-r--r--src/main/java/org/traccar/session/cache/CacheValue.java49
-rw-r--r--src/main/java/org/traccar/storage/query/Condition.java4
4 files changed, 324 insertions, 0 deletions
diff --git a/src/main/java/org/traccar/session/cache/CacheKey.java b/src/main/java/org/traccar/session/cache/CacheKey.java
new file mode 100644
index 000000000..23145e34b
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/CacheKey.java
@@ -0,0 +1,57 @@
+/*
+ * 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.Objects;
+
+class CacheKey {
+
+ private final Class<? extends BaseModel> clazz;
+ private final long id;
+
+ CacheKey(BaseModel object) {
+ this(object.getClass(), object.getId());
+ }
+
+ CacheKey(Class<? extends BaseModel> clazz, long id) {
+ this.clazz = clazz;
+ this.id = id;
+ }
+
+ public boolean classIs(Class<? extends BaseModel> clazz) {
+ return clazz.equals(this.clazz);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CacheKey cacheKey = (CacheKey) o;
+ return id == cacheKey.id && Objects.equals(clazz, cacheKey.clazz);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(clazz, id);
+ }
+
+}
diff --git a/src/main/java/org/traccar/session/cache/CacheManager.java b/src/main/java/org/traccar/session/cache/CacheManager.java
new file mode 100644
index 000000000..ae514ce8b
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/CacheManager.java
@@ -0,0 +1,214 @@
+/*
+ * 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.Attribute;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Command;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Geofence;
+import org.traccar.model.Maintenance;
+import org.traccar.model.Notification;
+import org.traccar.model.Order;
+import org.traccar.model.User;
+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;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+@Singleton
+public class CacheManager {
+
+ private static final Collection<Class<? extends BaseModel>> CLASSES = Arrays.asList(
+ Attribute.class, Command.class, Driver.class, Geofence.class,
+ Maintenance.class, Notification.class, Order.class);
+
+ private final Storage storage;
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private final Map<CacheKey, CacheValue> deviceCache = new HashMap<>();
+ private final Map<Long, Map<Class<? extends BaseModel>, List<Long>>> deviceLinks = new HashMap<>();
+
+ private final Map<Long, List<Notification>> userNotifications = new HashMap<>();
+
+ @Inject
+ public CacheManager(Storage storage) throws StorageException {
+ this.storage = storage;
+ invalidateUsers();
+ }
+
+ public <T extends BaseModel> T getObject(Class<T> clazz, long id) {
+ try {
+ lock.readLock().lock();
+ return deviceCache.get(new CacheKey(clazz, id)).getValue();
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ public <T extends BaseModel> List<T> getDeviceObjects(long deviceId, Class<T> clazz) {
+ try {
+ lock.readLock().lock();
+ return deviceLinks.get(deviceId).get(clazz).stream()
+ .map(id -> deviceCache.get(new CacheKey(clazz, id)).<T>getValue())
+ .collect(Collectors.toList());
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ public List<Notification> getUserNotifications(long userId) {
+ try {
+ lock.readLock().lock();
+ return userNotifications.get(userId);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ public void addDevice(long deviceId) throws StorageException {
+ try {
+ lock.writeLock().lock();
+ unsafeAddDevice(deviceId);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ public void removeDevice(long deviceId) {
+ try {
+ lock.writeLock().lock();
+ unsafeRemoveDevice(deviceId);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ public void invalidate(
+ Class<? extends BaseModel> clazz, long id) throws StorageException {
+ invalidate(new CacheKey(clazz, id));
+ }
+
+ public void invalidate(
+ Class<? extends BaseModel> clazz1, long id1,
+ Class<? extends BaseModel> clazz2, long id2) throws StorageException {
+ invalidate(new CacheKey(clazz1, id1), new CacheKey(clazz2, id2));
+ }
+
+ private void invalidateUsers() throws StorageException {
+ Map<Long, Notification> notifications = new HashMap<>();
+ storage.getObjects(Notification.class, new Request(new Columns.All()))
+ .forEach(notification -> notifications.put(notification.getId(), notification));
+ storage.getPermissions(User.class, Notification.class).forEach(permission -> {
+ long userId = permission.getOwnerId();
+ var notification = notifications.get(permission.getPropertyId());
+ userNotifications.computeIfAbsent(userId, k -> new LinkedList<>()).add(notification);
+ });
+ }
+
+ private void addObject(long deviceId, BaseModel object) {
+ deviceCache.computeIfAbsent(new CacheKey(object), k -> new CacheValue(object)).retain(deviceId);
+ }
+
+ private void unsafeAddDevice(long deviceId) throws StorageException {
+ Map<Class<? extends BaseModel>, List<Long>> links = new HashMap<>();
+
+ addObject(deviceId, storage.getObject(Device.class, new Request(
+ new Columns.All(), new Condition.Equals("id", "id", deviceId))));
+
+ 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.toList()));
+ objects.forEach(object -> addObject(deviceId, object));
+ }
+
+ 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.toList()));
+ for (var user : users) {
+ var notifications = storage.getObjects(Notification.class, new Request(
+ new Columns.All(), new Condition.Permission(User.class, user.getId(), Notification.class)));
+ notifications.stream()
+ .filter(Notification::getAlways)
+ .forEach(object -> {
+ links.computeIfAbsent(Notification.class, k -> new LinkedList<>()).add(object.getId());
+ addObject(deviceId, object);
+ });
+ }
+
+ deviceLinks.put(deviceId, links);
+ }
+
+ 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().size() > 0 ? value : null;
+ });
+ }));
+ }
+
+ 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 invalidateUsers = false;
+ Set<Long> linkedDevices = new HashSet<>();
+ for (var key : keys) {
+ if (key.classIs(User.class) || key.classIs(Notification.class)) {
+ invalidateUsers = true;
+ }
+ deviceCache.computeIfPresent(key, (k, value) -> {
+ linkedDevices.addAll(value.getReferences());
+ return value;
+ });
+ }
+ for (long deviceId : linkedDevices) {
+ unsafeRemoveDevice(deviceId);
+ unsafeAddDevice(deviceId);
+ }
+ if (invalidateUsers) {
+ invalidateUsers();
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/session/cache/CacheValue.java b/src/main/java/org/traccar/session/cache/CacheValue.java
new file mode 100644
index 000000000..9e955dfe5
--- /dev/null
+++ b/src/main/java/org/traccar/session/cache/CacheValue.java
@@ -0,0 +1,49 @@
+/*
+ * 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 final 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 Set<Long> getReferences() {
+ return references;
+ }
+
+}
diff --git a/src/main/java/org/traccar/storage/query/Condition.java b/src/main/java/org/traccar/storage/query/Condition.java
index 4cfdc907f..91ede236c 100644
--- a/src/main/java/org/traccar/storage/query/Condition.java
+++ b/src/main/java/org/traccar/storage/query/Condition.java
@@ -165,6 +165,10 @@ public interface Condition {
this(ownerClass, ownerId, propertyClass, 0, false);
}
+ public Permission(Class<?> ownerClass, Class<?> propertyClass, long propertyId) {
+ this(ownerClass, 0, propertyClass, propertyId, false);
+ }
+
public Permission excludeGroups() {
return new Permission(this.ownerClass, this.ownerId, this.propertyClass, this.propertyId, true);
}