Fix nop cache using (especially for testing) (#2841)

+ fix flaky tests that requires no caches

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-12-01 13:53:08 +02:00
committed by GitHub
parent b8a05e3cbf
commit 6988f5eafb
6 changed files with 100 additions and 27 deletions

View File

@@ -1,5 +1,4 @@
# org.springframework:spring-web:5.3.31.RELEASE, ineffective vulnerability - hawkBit doesn't use beans of type HttpInvokerServiceExporter in applications # org.eclipse.hawkbit:*
CVE-2016-1000027 # this trivy finding is false positive since the current master (and other, new temporary, branches) doesn't contain http dependencies anymore
# trivy detects 0-SNAPSHOT version and could not 'understand' that it is newer then 0.3.0M2 (since which the http dependencies were removed)
# org.yaml:snakeyaml:1.33, ineffective vulnerability - Not applicable. Applications does not consume user-provided YAML data CVE-2019-10240
CVE-2022-1471

View File

@@ -18,6 +18,7 @@ import java.util.concurrent.ConcurrentHashMap;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.NonNull; import lombok.NonNull;
import lombok.Value; import lombok.Value;
@@ -27,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache; import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@@ -122,6 +124,26 @@ public class TenantAwareCacheManager implements CacheManager {
} }
} }
// Nop if, null, blank, "nop", "none", "maximumSize=0" or "expireAfterWrite=0"
static boolean isNop(@Nullable String spec) {
if (spec == null || spec.isBlank()) {
return true;
}
final String trimmed = spec.replaceAll("\\s", "");
return "nop".equalsIgnoreCase(trimmed) ||
"none".equalsIgnoreCase(trimmed) ||
(trimmed.contains("maximumSize=0") &&
("maximumSize=0".equals(trimmed) ||
trimmed.startsWith("maximumSize=0,") ||
trimmed.contains(",maximumSize=0,") ||
trimmed.endsWith(",maximumSize=0"))) ||
(trimmed.contains("expireAfterWrite=0") &&
("expireAfterWrite=0".equals(trimmed) ||
trimmed.startsWith("expireAfterWrite=0,") ||
trimmed.contains(",expireAfterWrite=0,") ||
trimmed.endsWith(",expireAfterWrite=0")));
}
public interface CacheEvictEvent { public interface CacheEvictEvent {
String getTenant(); String getTenant();
@@ -161,6 +183,11 @@ public class TenantAwareCacheManager implements CacheManager {
spec = defaultSpec; spec = defaultSpec;
} }
} }
// it seems that setting maximumSize=0 doesn't filly disables the Caffeine cache, so we explicitly check for Nop cache
if (isNop(spec)) {
log.info("Using NOP cache for tenant '{}' and cache '{}'", tenant, name);
return new Nop(name);
}
try { try {
return new CaffeineCache(n, Caffeine.from(spec).build(), false) { return new CaffeineCache(n, Caffeine.from(spec).build(), false) {
@@ -188,35 +215,35 @@ public class TenantAwareCacheManager implements CacheManager {
} }
@Value @Value
private static class Nop implements Cache { @EqualsAndHashCode(callSuper = true)
private static class Nop extends AbstractValueAdaptingCache {
String name; String name;
private Nop(final String name) {
super(false);
this.name = name;
}
@NonNull @NonNull
@Override @Override
public String getName() { public String getName() {
return name; return name;
} }
@NonNull
@Override @Override
public Object getNativeCache() { public @NonNull Object getNativeCache() {
return this; return this;
} }
@Override @Override
public ValueWrapper get(@NonNull final Object key) { @SuppressWarnings("unchecked")
return null;
}
@Override
public <T> T get(@NonNull final Object key, final Class<T> type) {
return null;
}
@Override
public <T> T get(@NonNull final Object key, @NonNull final Callable<T> valueLoader) { public <T> T get(@NonNull final Object key, @NonNull final Callable<T> valueLoader) {
return null; try {
return (T) fromStoreValue(valueLoader.call());
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e);
}
} }
@Override @Override
@@ -233,5 +260,10 @@ public class TenantAwareCacheManager implements CacheManager {
public void clear() { public void clear() {
// nop // nop
} }
@Override
protected Object lookup(@NonNull final Object key) {
return null; // nop cache doesn't cache anything, especially used to DO NOT cache null values
}
} }
} }

View File

@@ -7,7 +7,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.eclipse.hawkbit.im.authentication; package org.eclipse.hawkbit.auth;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test;
* Feature: Unit Tests - Security<br/> * Feature: Unit Tests - Security<br/>
* Story: Permission Test * Story: Permission Test
*/ */
final class SpPermissionTest { class SpPermissionTest {
/** /**
* Double-checks that all permissions doesn't contain any hierarchies. * Double-checks that all permissions doesn't contain any hierarchies.

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collection;
import org.junit.jupiter.api.Test;
class TenantAwareCacheManagerTest {
/**
* Double-checks that all permissions doesn't contain any hierarchies.
*/
@Test
void testNopCaches() {
assertThat(TenantAwareCacheManager.isNop(null)).isTrue();
assertThat(TenantAwareCacheManager.isNop("")).isTrue();
assertThat(TenantAwareCacheManager.isNop(" ")).isTrue();
assertThat(TenantAwareCacheManager.isNop("\t")).isTrue();
assertThat(TenantAwareCacheManager.isNop("nop")).isTrue();
assertThat(TenantAwareCacheManager.isNop("none")).isTrue();
assertThat(TenantAwareCacheManager.isNop("maximumSize=0")).isTrue();
assertThat(TenantAwareCacheManager.isNop("maximumSize=0,something")).isTrue();
assertThat(TenantAwareCacheManager.isNop("something, maximumSize=0")).isTrue();
assertThat(TenantAwareCacheManager.isNop("something, maximumSize=0 \t, something")).isTrue();
assertThat(TenantAwareCacheManager.isNop(" expireAfterWrite=0")).isTrue();
assertThat(TenantAwareCacheManager.isNop("expireAfterWrite=0,something")).isTrue();
assertThat(TenantAwareCacheManager.isNop("something, expireAfterWrite =0")).isTrue();
assertThat(TenantAwareCacheManager.isNop("something, expireAfterWrite= 0 \t, something")).isTrue();
assertThat(TenantAwareCacheManager.isNop("maximumSize=01")).isFalse();
assertThat(TenantAwareCacheManager.isNop("maximumSize=100")).isFalse();
assertThat(TenantAwareCacheManager.isNop("expireAfterWrite=01")).isFalse();
assertThat(TenantAwareCacheManager.isNop("expireAfterWrite=100")).isFalse();
}
}

View File

@@ -23,9 +23,9 @@ hawkbit.controller.maintenanceWindowPollCount=3
# Cache config # Cache config
hawkbit.cache.ttl=10s hawkbit.cache.ttl=10s
hawkbit.cache.spec=expireAfterAccess=${hawkbit.cache.ttl} hawkbit.cache.spec=expireAfterWrite=${hawkbit.cache.ttl}
hawkbit.cache.RolloutStatus.spec=maximumSize=50000,expireAfterAccess=${hawkbit.cache.ttl} hawkbit.cache.RolloutStatus.spec=maximumSize=50000,expireAfterWrite=${hawkbit.cache.ttl}
hawkbit.cache.RolloutGroupStatus.spec=maximumSize=50000,expireAfterAccess=${hawkbit.cache.ttl} hawkbit.cache.RolloutGroupStatus.spec=maximumSize=50000,expireAfterWrite=${hawkbit.cache.ttl}
# Cache config - END # Cache config - END
# Attention: if you want to use a maximumPollingTime greater 23:59:59 you have to update the DurationField in the configuration window # Attention: if you want to use a maximumPollingTime greater 23:59:59 you have to update the DurationField in the configuration window

View File

@@ -89,9 +89,6 @@ class TenantConfigurationManagementTest extends AbstractJpaIntegrationTest imple
tenantConfigurationManagement.addOrUpdateConfiguration( tenantConfigurationManagement.addOrUpdateConfiguration(
TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue2); TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, newConfigurationValue2);
// sometimes it reads old value, maybe if read too early. wait to settle up?
waitMillis(100);
// verify that new configuration value is used // verify that new configuration value is used
final TenantConfigurationValue<String> updatedConfigurationValue2 = tenantConfigurationManagement final TenantConfigurationValue<String> updatedConfigurationValue2 = tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class); .getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, String.class);