001/**
002 * Copyright (C) 2006-2025 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.server.service.jcache;
017
018import static java.util.Optional.ofNullable;
019import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
020
021import java.lang.annotation.Annotation;
022import java.util.Collections;
023import java.util.Map;
024import java.util.concurrent.TimeUnit;
025import java.util.stream.StreamSupport;
026
027import javax.annotation.PostConstruct;
028import javax.annotation.PreDestroy;
029import javax.cache.Cache;
030import javax.cache.CacheManager;
031import javax.cache.annotation.CacheMethodDetails;
032import javax.cache.annotation.CacheResolver;
033import javax.cache.annotation.CacheResolverFactory;
034import javax.cache.annotation.CacheResult;
035import javax.cache.configuration.Configuration;
036import javax.enterprise.context.ApplicationScoped;
037import javax.inject.Inject;
038import javax.ws.rs.core.Response;
039
040import org.apache.geronimo.jcache.simple.cdi.CacheResolverImpl;
041import org.eclipse.microprofile.config.inject.ConfigProperty;
042import org.talend.sdk.component.api.meta.Documentation;
043import org.talend.sdk.component.server.api.CacheResource;
044import org.talend.sdk.component.server.front.ComponentResourceImpl;
045import org.talend.sdk.component.server.front.ConfigurationTypeResourceImpl;
046import org.talend.sdk.component.server.front.EnvironmentResourceImpl;
047import org.talend.sdk.component.server.front.model.Environment;
048import org.talend.sdk.component.server.service.ComponentManagerService;
049import org.talend.sdk.components.vault.jcache.CacheConfigurationFactory;
050import org.talend.sdk.components.vault.jcache.CacheSizeManager;
051
052import lombok.extern.slf4j.Slf4j;
053
054@Slf4j
055@ApplicationScoped
056public class FrontCacheResolver implements CacheResolverFactory, CacheResource {
057
058    @Inject
059    private CacheManager cacheManager;
060
061    @Inject
062    private CacheConfigurationFactory cacheConfiguration;
063
064    @Inject
065    @Documentation("How often (in ms) should we invalidate the credentials caches.")
066    @ConfigProperty(name = "talend.vault.cache.jcache.refresh.period", defaultValue = "30000")
067    private Long refreshPeriod;
068
069    @Inject
070    private ComponentManagerService service;
071
072    @Inject
073    EnvironmentResourceImpl env;
074
075    @Inject
076    ComponentResourceImpl components;
077
078    @Inject
079    ConfigurationTypeResourceImpl resources;
080
081    private long lastUpdated;
082
083    private volatile boolean running = true;
084
085    private Thread thread;
086
087    @PostConstruct
088    private void startRefresh() {
089        lastUpdated = System.currentTimeMillis();
090        thread = new Thread(() -> refreshThread(refreshPeriod));
091        thread.setName(getClass().getName() + "-refresher");
092        thread.setPriority(Thread.NORM_PRIORITY);
093        thread.setDaemon(false);
094        thread.setUncaughtExceptionHandler((t, e) -> log.error(e.getMessage(), e));
095        thread.start();
096    }
097
098    @PreDestroy
099    private void stopRefresh() {
100        running = false;
101        ofNullable(thread).ifPresent(it -> {
102            try {
103                it.interrupt();
104                it.join(TimeUnit.SECONDS.toMillis(5)); // not super important here
105            } catch (final InterruptedException e) {
106                log.warn(e.getMessage());
107                Thread.currentThread().interrupt();
108            }
109        });
110    }
111
112    private void refreshThread(final long delay) {
113        try {
114            while (running) {
115                try {
116                    updateIfNeeded();
117                } catch (final Exception e) {
118                    log.warn(e.getMessage(), e);
119                }
120                Thread.sleep(delay);
121            }
122        } catch (final InterruptedException ie) {
123            Thread.currentThread().interrupt();
124        }
125    }
126
127    private void updateIfNeeded() {
128        final Environment environment = env.get();
129        // assumes time are synch-ed but not a high assumption
130        if (lastUpdated < environment.getLastUpdated().getTime()) {
131            cleanupCaches();
132            lastUpdated = System.currentTimeMillis();
133        }
134    }
135
136    /**
137     * Clear all soft caches
138     */
139    public void cleanupCaches() {
140        StreamSupport
141                .stream(cacheManager.getCacheNames().spliterator(), false)
142                .filter(name -> name.startsWith("org.talend.sdk.component.server.front."))
143                .peek(c -> log.info("[clearCaches] clear cache {}.", c))
144                .forEach(r -> cacheManager.getCache(r).clear());
145        components.clearCache(null);
146        resources.clearCache(null);
147    }
148
149    @Override
150    public Response clearCaches() {
151        final long clearedCacheCount = countActiveCaches();
152        Map<String, Long> stringStringMap = Collections.singletonMap("clearedCacheCount", clearedCacheCount);
153        service.redeployPlugins();
154        return Response
155                .ok(stringStringMap)
156                .type(APPLICATION_JSON_TYPE)
157                .build();
158    }
159
160    /**
161     * mainly used for testing purpose.
162     * 
163     * @return active caches count
164     */
165    public Long countActiveCaches() {
166        return StreamSupport
167                .stream(cacheManager.getCacheNames().spliterator(), false)
168                .filter(name -> name.startsWith("org.talend.sdk.component.server.front."))
169                .filter(c -> cacheManager.getCache(c).iterator().hasNext())
170                .count();
171    }
172
173    @Override
174    public CacheResolver getCacheResolver(final CacheMethodDetails<? extends Annotation> cacheMethodDetails) {
175        return findCacheResolver(cacheMethodDetails.getCacheName());
176    }
177
178    @Override
179    public CacheResolver getExceptionCacheResolver(final CacheMethodDetails<CacheResult> cacheMethodDetails) {
180        return findCacheResolver(cacheMethodDetails.getCacheAnnotation().exceptionCacheName());
181    }
182
183    private CacheResolver findCacheResolver(final String exceptionCacheName) {
184        Cache<?, ?> cache = cacheManager.getCache(exceptionCacheName);
185        if (cache == null) {
186            try {
187                synchronized (this) {
188                    cache = createCache(exceptionCacheName);
189                }
190            } catch (final Exception ce) {
191                log.warn("[findCacheResolver] createCache failed: {}.", ce.getMessage());
192                cache = cacheManager.getCache(exceptionCacheName);
193            }
194        }
195        return new CacheResolverImpl(cache);
196    }
197
198    private Cache<?, ?> createCache(final String exceptionCacheName) {
199        log.debug("[createCache] {}", exceptionCacheName);
200        final CacheSizeManager<Object, Object> listener = new CacheSizeManager<>(cacheConfiguration.maxSize());
201        final Configuration<Object, Object> configuration = cacheConfiguration.createConfiguration(listener);
202        final Cache<Object, Object> instance = cacheManager.createCache(exceptionCacheName, configuration);
203        listener.accept(instance);
204        return instance;
205    }
206}