diff --git a/build.gradle.kts b/build.gradle.kts index 5b59a01a9a62f96292e19f14458376fd2cdf165b..486d4cd7ca0058cfa86b48739caecddcb6090250 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs import org.springframework.boot.gradle.tasks.bundling.BootJar group = "nl.tudelft.labrador" -version = "1.0.3" +version = "1.1.0" val javaVersion = JavaVersion.VERSION_11 diff --git a/src/main/java/nl/tudelft/librador/cache/CoreCacheManager.java b/src/main/java/nl/tudelft/librador/cache/CoreCacheManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e8a1f15c7deafb741664b8ff12aecc9d1d5467d5 --- /dev/null +++ b/src/main/java/nl/tudelft/librador/cache/CoreCacheManager.java @@ -0,0 +1,216 @@ +/* + * Librador - A utilities library for Labrador + * Copyright (C) 2020- Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.librador.cache; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +import com.google.common.collect.Sets; + +/* + * Stolen from Queue. + */ +public abstract class CoreCacheManager<ID, DTO> { + protected final Map<ID, DTO> cache = new ConcurrentHashMap<>(); + protected final Set<ID> empties = new HashSet<>(); + + /** + * Fetches the objects currently missing from the cache. These objects are fetched through a list of their + * IDs. + * + * @param ids The list of IDs of objects to fetch. + * @return The list of fetched objects. + */ + protected abstract List<DTO> fetch(List<ID> ids); + + /** + * Gets the ID of a particular instance of the data object. + * + * @param dto The DTO data object that will be cached. + * @return The ID of the DTO object. + */ + protected abstract ID getId(DTO dto); + + /** + * @return The maximum number of items to be fetched from Labracore in one go. + */ + protected int batchSize() { + return 128; + } + + /** + * Used to manually check the status of the cache and invalidate the cache if necessary. + */ + protected synchronized void validateCache() { + } + + /** + * Gets a list of DTOs from a list of ids. The list of ids gets broken up into already cached items and to + * be fetched items. The to be fetched are then fetched and the already cached are just fetched from + * cache. + * + * @param ids The ids to find matching DTOs for. + * @return The list of DTOs found to be matching. + */ + public List<DTO> get(List<ID> ids) { + validateCache(); + + List<ID> misses = ids.stream() + .filter(id -> !cache.containsKey(id) && !empties.contains(id)) + .collect(Collectors.toList()); + if (!misses.isEmpty()) { + forEachBatch(misses, batch -> registerImpl(new HashSet<>(batch), fetch(batch))); + } + + return ids.stream() + .map(cache::get) + .collect(Collectors.toList()); + } + + /** + * Gets a list of DTOs from a stream of IDs. This method is mostly used as a convenience method wrapping + * {@link #get(List)}. + * + * @param ids The stream of ids to be collected and requested from the cache. + * @return The list of DTOs with the given IDs. + */ + public List<DTO> get(Stream<ID> ids) { + return get(ids.collect(Collectors.toList())); + } + + /** + * Gets a single DTO from its id. If the DTO is already cached, no external lookup is done. + * + * @param id The id of the DTO to lookup. + * @return The requested DTO if it exists, otherwise nothing. + */ + public Optional<DTO> get(ID id) { + if (id == null) { + return Optional.empty(); + } + return Optional.of(get(List.of(id))) + .filter(l -> !l.isEmpty()) + .map(l -> l.get(0)); + } + + /** + * Gets the DTO with the given id or throws an exception with status code 404 if none such DTO could be + * found. + * + * @param id The id of the DTO to find. + * @return The found DTO with the given id. + */ + public DTO getOrThrow(ID id) { + var l = get(List.of(id)); + if (l == null || l.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find DTO by id " + id); + } + return l.get(0); + } + + /** + * Registers an entry into the cache after validating the cache first. + * + * @param dto The DTO that is to be registered. + */ + public void register(DTO dto) { + validateCache(); + registerImpl(dto); + } + + /** + * Registers a list of entries into the cache after validating the cache first. + * + * @param dtos The DTOs that are to be registered. + */ + public void register(List<DTO> dtos) { + validateCache(); + registerImpl(dtos); + } + + /** + * Implementation of the register method. Registers an entry into the cache. + * + * @param dto The DTO that is to be registered. + */ + private void registerImpl(DTO dto) { + cache.put(getId(dto), dto); + registerAdditionally(dto); + } + + /** + * Implementation of the register method. Registers a list of entries into the cache. + * + * @param dtos The DTOs that are to be registered. + */ + private void registerImpl(List<DTO> dtos) { + dtos.forEach(this::registerImpl); + } + + /** + * Implementation of the register method. Registers a list of entries into the cache and also registers + * the IDs that were requested but not returned as 'empty' IDs. + * + * @param requests The set of requested IDs. + * @param dtos The list of DTOs returned from the request. + */ + private void registerImpl(Set<ID> requests, List<DTO> dtos) { + dtos.forEach(this::registerImpl); + empties.addAll(Sets.difference(requests, + dtos.stream().map(this::getId).collect(Collectors.toSet()))); + } + + /** + * Performs additional registers in other caches if necessary. + * + * @param dto The DTO that is being registered. + */ + protected void registerAdditionally(DTO dto) { + } + + /** + * Helper function for iterating over batches of items. The batch size is determined through the + * {@link #batchSize()} method in implementations of this class. Each created batch will be passed on to + * the consumer function f. + * + * @param ids The list of ids that are to be batched. + * @param f The consumer of each batch. + */ + private void forEachBatch(List<ID> ids, Consumer<List<ID>> f) { + List<ID> batch = new ArrayList<>(); + for (ID id : ids) { + batch.add(id); + + if (batch.size() >= batchSize()) { + f.accept(batch); + batch.clear(); + } + } + + if (!batch.isEmpty()) { + f.accept(batch); + } + } + +} diff --git a/src/main/java/nl/tudelft/librador/cache/TimedCacheManager.java b/src/main/java/nl/tudelft/librador/cache/TimedCacheManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4dce39fa71c7a9344d8fcefe0f2940562e681b5d --- /dev/null +++ b/src/main/java/nl/tudelft/librador/cache/TimedCacheManager.java @@ -0,0 +1,66 @@ +/* + * Librador - A utilities library for Labrador + * Copyright (C) 2020- Delft University of Technology + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ +package nl.tudelft.librador.cache; + +/* + * Stolen from Queue. + */ +public abstract class TimedCacheManager<ID, DTO> extends CoreCacheManager<ID, DTO> { + /** + * The default timeout of the timed cache. + */ + public static final long DEFAULT_TIMEOUT = 60000L; + + /** + * The set timeout of the timed cache in milliseconds. This many milliseconds after the last cache + * invalidation, this cache is invalidated again. + */ + private final long timeout; + + /** + * The last time in milliseconds since epoch (system-time) that this cache was (in)validated. + * {@link #timeout} milliseconds after this timestamp, the cache is invalidated (again). + */ + private long lastValidation = System.currentTimeMillis(); + + /** + * Constructs a new timed cache with the timeout set to {@link #DEFAULT_TIMEOUT}. + */ + public TimedCacheManager() { + this.timeout = DEFAULT_TIMEOUT; + } + + /** + * Constructs a new timed cache with the timeout set to the given timeout. + * + * @param timeout The timeout to set internally. + */ + public TimedCacheManager(long timeout) { + this.timeout = timeout; + } + + @Override + protected synchronized void validateCache() { + long now = System.currentTimeMillis(); + if (lastValidation + timeout < now) { + cache.clear(); + empties.clear(); + lastValidation = now; + } + } +}