diff options
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); } |