Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Align togglz with virtual threads #1172

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.collections4.map.PassiveExpiringMap;
import org.togglz.servlet.spi.CSRFToken;

public class TogglzCSRFTokenCache {

private static final PassiveExpiringMap<String, CSRFToken> expiringMap;
private static final Object lock = new Object();
private static final ReentrantLock lock = new ReentrantLock();

static {
PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<String, CSRFToken>
Expand All @@ -19,15 +20,20 @@ public class TogglzCSRFTokenCache {
}

static void cacheToken(CSRFToken token) {
synchronized (lock) {
expiringMap.put(token.getValue(), token);
}
lock.lock();
try {
expiringMap.put(token.getValue(), token);
} finally {
lock.unlock();
}
}

static boolean isTokenInCache(CSRFToken token) {
synchronized (lock) {
lock.lock();
try {
return expiringMap.containsKey(token.getValue());
}
} finally {
lock.unlock();
}
}

}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.togglz.core.repository.cache;

import java.time.Clock;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.togglz.core.Feature;
import org.togglz.core.repository.FeatureState;
import org.togglz.core.repository.StateRepository;
Expand All @@ -22,6 +26,13 @@ public class CachingStateRepository implements StateRepository {

private final long ttl;

private final Map<Feature, Lock> locks = new ConcurrentHashMap<>();

private final ExecutorService executorService;

//visible for tests
static Clock clock = Clock.systemUTC();

/**
* Creates a caching facade for the supplied {@link StateRepository}. The cached state of a feature will only expire if
* {@link #setFeatureState(FeatureState)} is invoked. You should therefore never use this constructor if the feature state
Expand All @@ -42,45 +53,91 @@ public CachingStateRepository(StateRepository delegate) {
* @throws IllegalArgumentException if the specified ttl is negative
*/
public CachingStateRepository(StateRepository delegate, long ttl) {
this(delegate, ttl, (ExecutorService) null);
}

/**
* Creates a caching facade for the supplied {@link StateRepository}. The cached state of a feature will expire after the
* supplied TTL rounded down to milliseconds or if {@link #setFeatureState(FeatureState)} is invoked.
*
* @param delegate The repository to delegate invocations to
* @param ttl The time in a given {@code ttlTimeUnit} after which a cache entry will expire
* @param ttlTimeUnit The unit that {@code ttl} is expressed in
*/
public CachingStateRepository(StateRepository delegate, long ttl, TimeUnit ttlTimeUnit) {
this(delegate, ttlTimeUnit.toMillis(ttl));
}

/**
* Creates a caching facade for the supplied {@link StateRepository}. The cached state of a feature will expire after the
* supplied TTL rounded down to milliseconds or if {@link #setFeatureState(FeatureState)} is invoked.
*
* @param delegate The repository to delegate invocations to
* @param ttl The time in milliseconds after which a cache entry will expire
* @param executorService The thread pool for scheduling async refreshes of cache entries, if not provided entries would be reloaded synchronously, when
* item is not in cache null will be returned
* @throws IllegalArgumentException if the specified ttl is negative
*/
public CachingStateRepository(StateRepository delegate, long ttl, ExecutorService executorService) {
if (ttl < 0) {
throw new IllegalArgumentException("Negative TTL value: " + ttl);
}

this.delegate = delegate;
this.ttl = ttl;
this.executorService = executorService;
}

/**
* Creates a caching facade for the supplied {@link StateRepository}. The cached state of a feature will expire after the
* supplied TTL rounded down to milliseconds or if {@link #setFeatureState(FeatureState)} is invoked.
*
* @param delegate The repository to delegate invocations to
* @param ttl The time in a given {@code ttlTimeUnit} after which a cache entry will expire
* @param ttlTimeUnit The unit that {@code ttl} is expressed in
* @param delegate The repository to delegate invocations to
* @param ttl The time in a given {@code ttlTimeUnit} after which a cache entry will expire
* @param ttlTimeUnit The unit that {@code ttl} is expressed in
* @param executorService The thread pool for scheduling async refreshes of cache entries, if not provided entries would be reloaded synchronously, when
* item is not in cache null will be returned
*/
public CachingStateRepository(StateRepository delegate, long ttl, TimeUnit ttlTimeUnit) {
this(delegate, ttlTimeUnit.toMillis(ttl));
public CachingStateRepository(StateRepository delegate, long ttl, TimeUnit ttlTimeUnit, ExecutorService executorService) {
this(delegate, ttlTimeUnit.toMillis(ttl), executorService);
}

@Override
public FeatureState getFeatureState(Feature feature) {
// first try to find it from the cache
CacheEntry entry = cache.get(feature.name());
if (isValidEntry(entry)) {
return entry.getState();
if (asyncReload()) {
if (entry == null || entry.isExpired()) {
executorService.execute(() -> reloadFeatureState(feature));
}
return entry == null ? null : entry.getState();
} else {
if (isValidEntry(entry)) {
return entry.getState();
}
// no cache hit
return reloadFeatureState(feature);
}
// no cache hit
return reloadFeatureState(feature);
}

private synchronized FeatureState reloadFeatureState(Feature feature) {
CacheEntry cachedState = cache.get(feature.name());
if (isValidEntry(cachedState)) {
return cachedState.getState();
private boolean asyncReload() {
return executorService != null;
}

private FeatureState reloadFeatureState(Feature feature) {
locks.computeIfAbsent(feature, it -> new ReentrantLock())
.lock();
try {
CacheEntry cachedState = cache.get(feature.name());
if (isValidEntry(cachedState)) {
return cachedState.getState();
}
FeatureState featureState = delegate.getFeatureState(feature);
storeFeatureState(feature, featureState);
return featureState;
} finally {
locks.get(feature).unlock();
}
FeatureState featureState = delegate.getFeatureState(feature);
storeFeatureState(feature, featureState);
return featureState;
}

private void storeFeatureState(Feature feature, FeatureState featureState) {
Expand Down Expand Up @@ -117,7 +174,7 @@ private static class CacheEntry {

public CacheEntry(FeatureState state, final long ttl) {
this.state = state;
this.timestamp = System.currentTimeMillis();
this.timestamp = clock.millis();
this.ttl = ttl;
}

Expand All @@ -129,7 +186,7 @@ public boolean isExpired() {
if (ttl == 0) {
return false;
}
return timestamp + ttl < System.currentTimeMillis();
return timestamp + ttl < clock.millis();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Properties;
import java.util.Set;

import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togglz.core.repository.property.PropertySource;
Expand All @@ -32,55 +33,61 @@ class ReloadablePropertiesFile implements PropertySource {

private long lastCheck = 0;

private final ReentrantLock lock = new ReentrantLock();

public ReloadablePropertiesFile(File file, int minCheckInterval) {
this.file = file;
this.minCheckInterval = minCheckInterval;
}

public synchronized void reloadIfUpdated() {
if (!this.file.exists()) {
try {
if (this.file.createNewFile()) {
log.debug("Created non-existent file.");
public void reloadIfUpdated() {
lock.lock();
try {
if (!this.file.exists()) {
try {
if (this.file.createNewFile()) {
log.debug("Created non-existent file.");
}
} catch (IOException e) {
log.error("Error creating missing file " + this.file.getName(), e);
}
} catch (IOException e) {
log.error("Error creating missing file " + this.file.getName(), e);
}
}

long now = System.currentTimeMillis();
if (now - lastCheck > minCheckInterval) {
long now = System.currentTimeMillis();
if (now - lastCheck > minCheckInterval) {

lastCheck = now;
lastCheck = now;

if (file.lastModified() > lastRead) {
if (file.lastModified() > lastRead) {

FileInputStream stream = null;
FileInputStream stream = null;

try {
try {

// read new values
stream = new FileInputStream(file);
Properties newValues = new Properties();
newValues.load(stream);
// read new values
stream = new FileInputStream(file);
Properties newValues = new Properties();
newValues.load(stream);

// update state
values = newValues;
lastRead = System.currentTimeMillis();
// update state
values = newValues;
lastRead = System.currentTimeMillis();

log.info("Reloaded file: " + file.getCanonicalPath());
log.info("Reloaded file: " + file.getCanonicalPath());

} catch (FileNotFoundException e) {
log.debug("File not found: " + file);
} catch (IOException e) {
log.error("Failed to read file", e);
} finally {
IOUtils.close(stream);
}
} catch (FileNotFoundException e) {
log.debug("File not found: " + file);
} catch (IOException e) {
log.error("Failed to read file", e);
} finally {
IOUtils.close(stream);
}

}
}
} finally {
lock.unlock();
}

}

public String getValue(String key, String defaultValue) {
Expand Down
Loading