All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.fluxcapacitor.javaclient.persisting.caching.DefaultCache Maven / Gradle / Ivy

There is a newer version: v0.1
Show newest version
/*
 * Copyright (c) Flux Capacitor IP B.V. or its affiliates. All Rights Reserved.
 *
 * 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 io.fluxcapacitor.javaclient.persisting.caching;


import io.fluxcapacitor.common.Registration;
import io.fluxcapacitor.javaclient.FluxCapacitor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import static io.fluxcapacitor.common.ObjectUtils.newThreadFactory;
import static io.fluxcapacitor.javaclient.persisting.caching.CacheEvictionEvent.Reason.manual;
import static io.fluxcapacitor.javaclient.persisting.caching.CacheEvictionEvent.Reason.memoryPressure;
import static io.fluxcapacitor.javaclient.persisting.caching.CacheEvictionEvent.Reason.size;

@AllArgsConstructor
@Slf4j
public class DefaultCache implements Cache, AutoCloseable {
    protected static final String mutexPrecursor = "$DC$";

    @Getter
    private final Map valueMap;

    private final Executor evictionNotifier;
    private final Duration expiry;
    private final Supplier clockSupplier;

    private final Collection> evictionListeners = new CopyOnWriteArrayList<>();

    private final ScheduledExecutorService referencePurger = Executors.newScheduledThreadPool(
            2, newThreadFactory("DefaultCache-referencePurger"));

    private final ReferenceQueue referenceQueue = new ReferenceQueue<>();

    public DefaultCache() {
        this(1_000_000);
    }

    public DefaultCache(int maxSize) {
        this(maxSize, null);
    }

    public DefaultCache(int maxSize, Duration expiry) {
        this(maxSize, Executors.newSingleThreadExecutor(newThreadFactory("DefaultCache-evictionNotifier")), expiry);
    }

    public DefaultCache(int maxSize, Executor evictionNotifier, Duration expiry) {
        this(maxSize, evictionNotifier, expiry, Duration.ofMinutes(1));
    }

    public DefaultCache(int maxSize, Executor evictionNotifier, Duration expiry, Duration expiryCheckDelay) {
        this.valueMap = Collections.synchronizedMap(new LinkedHashMap<>(Math.min(128, maxSize), 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                boolean remove = size() > maxSize;
                try {
                    return remove;
                } finally {
                    if (remove) {
                        notifyEvictionListeners(eldest.getKey(), size);
                    }
                }
            }
        });
        this.evictionNotifier = evictionNotifier;
        this.clockSupplier = FluxCapacitor.getOptionally().>map(fc -> fc::clock)
                .orElseGet(() -> Clock::systemUTC);
        this.referencePurger.execute(this::pollReferenceQueue);
        if ((this.expiry = expiry) != null) {
            this.referencePurger.scheduleWithFixedDelay(
                    this::removeExpiredReferences, expiryCheckDelay.toMillis(), expiryCheckDelay.toMillis(),
                    TimeUnit.MILLISECONDS);
        }
    }

    @Override
    public  T compute(Object id, BiFunction mappingFunction) {
        synchronized ((mutexPrecursor + id).intern()) {
            CacheReference previous = valueMap.get(id);
            CacheReference next = wrap(id, mappingFunction.apply(id, unwrap(previous)));
            if (next == null) {
                valueMap.remove(id);
                if (previous != null && previous.get() != null) {
                    notifyEvictionListeners(id, manual);
                }
            } else {
                valueMap.put(id, next);
            }
            return unwrap(next);
        }
    }

    @Override
    public  void modifyEach(BiFunction modifierFunction) {
        new HashSet<>(valueMap.keySet()).forEach(key -> computeIfPresent(key, modifierFunction));
    }

    @Override
    public Object put(Object id, Object value) {
        return compute(id, (k, v) -> value == null ? Optional.empty() : value);
    }

    @Override
    public Object putIfAbsent(Object id, Object value) {
        return computeIfAbsent(id, k -> value == null ? Optional.empty() : value);
    }

    @Override
    public  T computeIfAbsent(Object id, Function mappingFunction) {
        return compute(id, (k, v) -> v == null ? mappingFunction.apply(k) : v);
    }

    @Override
    public  T computeIfPresent(Object id, BiFunction mappingFunction) {
        return compute(id, (k, v) -> v == null ? null : mappingFunction.apply(k, v));
    }

    @Override
    public  T remove(Object id) {
        return compute(id, (k, v) -> null);
    }

    @Override
    public  T get(Object id) {
        return unwrap(valueMap.get(id));
    }

    @Override
    public boolean containsKey(Object id) {
        return valueMap.containsKey(id);
    }

    @Override
    public void clear() {
        valueMap.clear();
        notifyEvictionListeners(null, manual);
    }

    @Override
    public int size() {
        return valueMap.size();
    }

    @Override
    public Registration registerEvictionListener(Consumer listener) {
        evictionListeners.add(listener);
        return () -> evictionListeners.remove(listener);
    }

    @Override
    public void close() {
        try {
            if (evictionNotifier instanceof ExecutorService executorService) {
                executorService.shutdownNow();
            }
            referencePurger.shutdownNow();
        } catch (Throwable ignored) {
        }
    }

    protected CacheReference wrap(Object id, Object value) {
        return value == null ? null : new CacheReference(id, value);
    }

    @SuppressWarnings("unchecked")
    protected  T unwrap(CacheReference ref) {
        if (ref == null) {
            return null;
        }
        if (ref.hasExpired()) {
            return null;
        }
        Object result = ref.get();
        if (result instanceof Optional) {
            result = ((Optional) result).orElse(null);
        }
        return (T) result;
    }

    protected void pollReferenceQueue() {
        try {
            Reference reference;
            while ((reference = referenceQueue.remove()) != null) {
                if (reference instanceof CacheReference cacheReference) {
                    remove(cacheReference.id);
                    notifyEvictionListeners(cacheReference.id, memoryPressure);
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    protected void removeExpiredReferences() {
        valueMap.entrySet().removeIf(e -> {
            if (e.getValue().hasExpired()) {
                notifyEvictionListeners(e.getKey(), CacheEvictionEvent.Reason.expiry);
                return true;
            }
            return false;
        });
    }

    protected void notifyEvictionListeners(Object id, CacheEvictionEvent.Reason reason) {
        var event = new CacheEvictionEvent(id, reason);
        evictionNotifier.execute(() -> evictionListeners.forEach(l -> l.accept(event)));
    }

    protected class CacheReference extends SoftReference {
        private final Object id;
        private final Instant deadline;

        public CacheReference(Object id, Object value) {
            super(value, referenceQueue);
            this.id = id;
            this.deadline = expiry == null ? null : clockSupplier.get().instant().plus(expiry);
        }

        boolean hasExpired() {
            return deadline != null && deadline.isBefore(clockSupplier.get().instant());
        }
    }
}