001/**
002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.runtime.manager.service;
017
018import java.io.ObjectStreamException;
019import java.io.Serializable;
020import java.util.List;
021import java.util.Map.Entry;
022import java.util.Objects;
023import java.util.Optional;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.ConcurrentMap;
026import java.util.concurrent.ScheduledExecutorService;
027import java.util.concurrent.ScheduledFuture;
028import java.util.concurrent.TimeUnit;
029import java.util.function.Function;
030import java.util.function.Predicate;
031import java.util.function.Supplier;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034
035import javax.annotation.PreDestroy;
036
037import org.talend.sdk.component.api.configuration.Option;
038import org.talend.sdk.component.api.service.cache.LocalCache;
039import org.talend.sdk.component.api.service.configuration.Configuration;
040import org.talend.sdk.component.runtime.serialization.SerializableService;
041
042import lombok.Data;
043
044/**
045 * Implementation of LocalCache with in memory concurrent map.
046 */
047public class LocalCacheService implements LocalCache, Serializable {
048
049    /** plugin name for this cache */
050    private final String plugin;
051
052    private final Supplier<Long> timer;
053
054    private final ConcurrentMap<String, ElementImpl> cache = new ConcurrentHashMap<>();
055
056    @Configuration("talend.component.manager.services.cache.eviction")
057    private Supplier<CacheConfiguration> configuration;
058
059    // scheduler we use to evict tokens
060    private transient Supplier<ScheduledExecutorService> threadServiceGetter;
061
062    public LocalCacheService(final String plugin, final Supplier<Long> timer,
063            final Supplier<ScheduledExecutorService> threadServiceGetter) {
064        this.plugin = plugin;
065        this.timer = timer;
066        this.threadServiceGetter = threadServiceGetter;
067    }
068
069    /**
070     * Evict an object.
071     * 
072     * @param key key to evict.
073     */
074    @Override
075    public void evict(final String key) {
076        final String realKey = internalKey(key);
077
078        // use compute to be able to call release.
079        cache.compute(realKey, (String oldKey, ElementImpl oldElement) -> {
080            if (oldElement != null && oldElement.canBeEvict()) {
081                // ok to evict, so do release.
082                oldElement.release();
083                return null;
084            }
085            return oldElement;
086        });
087    }
088
089    @Override
090    public void evictIfValue(final String key, final Object expected) {
091        final String realKey = internalKey(key);
092
093        // use compute to be able to call release.
094        cache.compute(realKey, (String oldKey, ElementImpl oldElement) -> {
095            if (oldElement != null && (Objects.equals(oldElement.getValue(), expected) || oldElement.canBeEvict())) {
096                // ok to evit, so do release.
097                oldElement.release();
098                return null;
099            }
100            return oldElement;
101        });
102
103    }
104
105    @Override
106    public <T> T computeIfAbsent(final Class<T> expectedClass, final String key, final Predicate<Element> toRemove,
107            final long timeoutMs, final Supplier<T> value) {
108
109        final Integer maxSize = this.getConfigValue(CacheConfiguration::getDefaultMaxSize, -1);
110        if (maxSize > 0 && this.cache.size() >= maxSize) {
111            this.clean(); // clean before add one element.
112            if (this.cache.size() >= maxSize) {
113                synchronized (this.cache) {
114                    while (this.cache.size() >= maxSize) {
115                        final String keyToRemove = this.cache.keySet().iterator().next();
116                        this.cache.remove(keyToRemove);
117                    }
118                }
119            }
120        }
121
122        final ScheduledFuture<?> task = timeoutMs > 0 ? this.evictionTask(key, timeoutMs) : null;
123
124        final long endOfValidity = this.calcEndOfValidity(timeoutMs);
125        final ElementImpl element =
126                this.addToMap(key, () -> new ElementImpl(value, toRemove, endOfValidity, task, this.timer));
127
128        return element.getValue(expectedClass);
129    }
130
131    @Override
132    public <T> T computeIfAbsent(final Class<T> expectedClass, final String key, final Predicate<Element> toRemove,
133            final Supplier<T> value) {
134
135        final long timeout = this.getConfigValue(CacheConfiguration::getDefaultEvictionTimeout, -1L);
136        return this.computeIfAbsent(expectedClass, key, toRemove, timeout, value);
137    }
138
139    @Override
140    public <T> T computeIfAbsent(final Class<T> expectedClass, final String key, final long timeoutMs,
141            final Supplier<T> value) {
142        return this.computeIfAbsent(expectedClass, key, null, timeoutMs, value);
143    }
144
145    private ElementImpl addToMap(final String key, final Supplier<ElementImpl> builder) {
146        final String internalKey = internalKey(key);
147        return cache.compute(internalKey, (String k, ElementImpl old) -> //
148        old == null || old.mustBeRemoved() ? builder.get() : old);
149    }
150
151    @Override
152    public <T> T computeIfAbsent(final Class<T> expectedClass, final String key, final Supplier<T> value) {
153        final long timeOut = this.getConfigValue(CacheConfiguration::getDefaultEvictionTimeout, -1L);
154        return computeIfAbsent(expectedClass, key, null, timeOut, value);
155    }
156
157    @PreDestroy
158    public void release() {
159        this.cache.forEach((String k, ElementImpl e) -> e.release());
160        this.cache.clear();
161    }
162
163    private long calcEndOfValidity(final long timeoutMs) {
164        return timeoutMs > 0 ? this.timer.get() + timeoutMs : -1;
165    }
166
167    private String internalKey(final String key) {
168        return plugin + '@' + key;
169    }
170
171    public void clean() {
172        Stream<Entry<String, ElementImpl>> elements = //
173                this.cache
174                        .entrySet() //
175                        .stream() //
176                        .filter(e -> e.getValue().mustBeRemoved());
177
178        final int maxEviction = this.getConfigValue(CacheConfiguration::getMaxDeletionPerEvictionRun, -1);
179        if (maxEviction > 0) {
180            elements = elements.limit(maxEviction);
181        }
182        final List<String> removableElements = elements.map(Entry::getKey).collect(Collectors.toList());// materialize
183                                                                                                        // before
184                                                                                                        // actually
185                                                                                                        // removing it
186        removableElements.forEach(this.cache::remove);
187    }
188
189    private ScheduledExecutorService getThreadService() {
190        return this.threadServiceGetter.get();
191    }
192
193    /**
194     * Schedule an eviction task for a key.
195     * 
196     * @param key : key to evict from cache.
197     * @param delayMillis : delay in millis before triggered.
198     * @return result of task.
199     */
200    private ScheduledFuture<?> evictionTask(final String key, final long delayMillis) {
201        return this
202                .getThreadService()
203                .schedule(() -> this.evict(key), //
204                        delayMillis, //
205                        TimeUnit.MILLISECONDS); //
206    }
207
208    private <T> T getConfigValue(final Function<CacheConfiguration, T> getter, final T defaultValue) {
209        return Optional
210                .ofNullable(this.getConfig()) //
211                .map(getter) //
212                .orElse(defaultValue);
213    }
214
215    private CacheConfiguration getConfig() {
216        return this.configuration != null ? this.configuration.get() : null;
217    }
218
219    /**
220     * Cache configuration.
221     */
222    @Data
223    public static class CacheConfiguration implements Serializable {
224
225        @Option
226        private long defaultEvictionTimeout;
227
228        @Option
229        private int maxDeletionPerEvictionRun;
230
231        @Option
232        private int defaultMaxSize;
233    }
234
235    /**
236     * Wrapper for each cached object.
237     */
238    private static class ElementImpl implements Element {
239
240        /** cached object */
241        private final Object value;
242
243        /** function, if exists, that authorize to remove object. */
244        private final Predicate<Element> canBeRemoved;
245
246        /** give time object can be release (infinity if < 0) */
247        private final long endOfValidity;
248
249        /** scheduled task to remove object if nedeed (to cancel if removed before) */
250        private final ScheduledFuture<?> removedTask;
251
252        private final Supplier<Long> serviceTimer;
253
254        public <T> ElementImpl(final Supplier<T> value, final Predicate<Element> canBeRemoved, final long endOfValidity,
255                final ScheduledFuture<?> removedTask, final Supplier<Long> timer) {
256            this.value = value.get();
257            this.canBeRemoved = canBeRemoved;
258            this.endOfValidity = endOfValidity;
259            this.removedTask = removedTask;
260            this.serviceTimer = timer;
261        }
262
263        @Override
264        public <T> T getValue(final Class<T> expectedType) {
265            if (this.value != null && !expectedType.isInstance(this.value)) {
266                throw new ClassCastException(
267                        this.value.getClass().getName() + " cannot be cast to " + expectedType.getName());
268            }
269            return expectedType.cast(this.value);
270        }
271
272        @Override
273        public long getLastValidityTimestamp() {
274            return this.endOfValidity;
275        }
276
277        public boolean mustBeRemoved() {
278            return (this.endOfValidity > 0 && this.endOfValidity <= this.serviceTimer.get()) // time out passed
279                    && this.canBeEvict(); // or function indicate to remove.
280        }
281
282        public boolean canBeEvict() {
283            return this.canBeRemoved == null || this.canBeRemoved.test(this);
284        }
285
286        /**
287         * Release this object because removed.
288         */
289        public synchronized void release() {
290            if (this.removedTask != null) {
291                this.removedTask.cancel(false);
292            }
293        }
294
295        @Override
296        public boolean equals(final Object o) { // consider only value
297            if (this == o) {
298                return true;
299            }
300            if (o == null || getClass() != o.getClass()) {
301                return false;
302            }
303            return Objects.equals(ElementImpl.class.cast(o).value, value);
304        }
305
306        @Override
307        public int hashCode() {
308            return Objects.hash(value);
309        }
310    }
311
312    Object writeReplace() throws ObjectStreamException {
313        return new SerializableService(plugin, LocalCache.class.getName());
314    }
315}