From 1d83e8d4239f87a7dc2cfaf1ed52244b86dcdbf6 Mon Sep 17 00:00:00 2001 From: Sammy Aknan Date: Tue, 13 Jan 2026 17:47:13 -0500 Subject: [PATCH] feat: add CachedValue#refresher --- .../net/staticstudios/data/CachedValue.java | 25 ++++++++++++++- .../data/impl/data/AbstractCachedValue.java | 18 +++++++++++ .../data/impl/data/CachedValueImpl.java | 25 ++++++++++++++- .../data/impl/data/ReadOnlyCachedValue.java | 13 +++++++- .../data/util/CachedValueRefresher.java | 8 +++++ .../staticstudios/data/CachedValueTest.java | 31 +++++++++++++++++-- .../data/mock/user/MockUser.java | 14 +++++++++ 7 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/net/staticstudios/data/util/CachedValueRefresher.java diff --git a/core/src/main/java/net/staticstudios/data/CachedValue.java b/core/src/main/java/net/staticstudios/data/CachedValue.java index 224d478..f0f338c 100644 --- a/core/src/main/java/net/staticstudios/data/CachedValue.java +++ b/core/src/main/java/net/staticstudios/data/CachedValue.java @@ -1,13 +1,13 @@ package net.staticstudios.data; import com.google.common.base.Preconditions; -import com.google.common.base.Supplier; import net.staticstudios.data.impl.data.AbstractCachedValue; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; /** * A cached value represents a piece of data in redis. @@ -30,14 +30,19 @@ default CachedValue withFallback(T fallback) { return supplyFallback(() -> fallback); } + CachedValue refresher(Class clazz, CachedValueRefresher refresher); + CachedValue supplyFallback(Supplier fallback); + @Nullable T refresh(); + class ProxyCachedValue implements CachedValue { protected final UniqueData holder; protected final Class dataType; private final List> updateHandlers = new ArrayList<>(); private @Nullable CachedValue delegate; private Supplier fallback = () -> null; + private @Nullable CachedValueRefresher refresher; public ProxyCachedValue(UniqueData holder, Class dataType) { this.holder = holder; @@ -48,6 +53,7 @@ public void setDelegate(CachedValueMetadata metadata, AbstractCachedValue del Preconditions.checkNotNull(delegate, "Delegate cannot be null"); Preconditions.checkState(this.delegate == null, "Delegate is already set"); delegate.setFallback(this.fallback); + delegate.setRefresher(refresher); this.delegate = delegate; //since an update handler can be registered before the fallback is set, we need to convert them here @@ -88,6 +94,15 @@ public CachedValue supplyFallback(Supplier fallback) { return this; } + @SuppressWarnings("unchecked") + @Override + public CachedValue refresher(Class clazz, CachedValueRefresher refresher) { + Preconditions.checkArgument(delegate == null, "Cannot dynamically add a refresher after the holder has been initialized!"); + LambdaUtils.assertLambdaDoesntCapture(refresher, List.of(UniqueData.class), null); + this.refresher = (CachedValueRefresher) refresher; + return this; + } + @Override public T get() { if (delegate != null) { @@ -105,6 +120,14 @@ public void set(@Nullable T value) { throw new UnsupportedOperationException("Not implemented"); } + @Override + public @Nullable T refresh() { + if (delegate != null) { + return delegate.refresh(); + } + throw new UnsupportedOperationException("Not implemented"); + } + private CachedValueUpdateHandlerWrapper asCachedValueHandler(ValueUpdateHandlerWrapper handlerWrapper) { return new CachedValueUpdateHandlerWrapper<>( handlerWrapper.getHandler(), diff --git a/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java b/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java index 1a95d68..a457fc1 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/AbstractCachedValue.java @@ -1,20 +1,38 @@ package net.staticstudios.data.impl.data; import net.staticstudios.data.CachedValue; +import net.staticstudios.data.UniqueData; +import net.staticstudios.data.util.CachedValueRefresher; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import java.util.function.Supplier; public abstract class AbstractCachedValue implements CachedValue { private Supplier fallbackSupplier; + private CachedValueRefresher refreshFunction; + @ApiStatus.Internal public void setFallback(Supplier fallbackSupplier) { this.fallbackSupplier = fallbackSupplier; } + @ApiStatus.Internal + public void setRefresher(CachedValueRefresher refreshFunction) { + this.refreshFunction = refreshFunction; + } + protected T getFallback() { if (fallbackSupplier != null) { return fallbackSupplier.get(); } return null; } + + protected T calculateRefreshedValue(@Nullable T currentValue) { + if (refreshFunction != null) { + return refreshFunction.apply(getHolder(), currentValue); + } + return null; + } } diff --git a/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java index 7d09804..7069a70 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/CachedValueImpl.java @@ -1,7 +1,6 @@ package net.staticstudios.data.impl.data; import com.google.common.base.Preconditions; -import com.google.common.base.Supplier; import net.staticstudios.data.CachedValue; import net.staticstudios.data.ExpireAfter; import net.staticstudios.data.Identifier; @@ -13,6 +12,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; public class CachedValueImpl extends AbstractCachedValue { private final UniqueData holder; @@ -95,6 +95,11 @@ public CachedValue onUpdate(Class holderClass, Valu throw new UnsupportedOperationException("Dynamically adding update handlers is not supported"); } + @Override + public CachedValue refresher(Class clazz, CachedValueRefresher refresher) { + throw new UnsupportedOperationException("Cannot set refresher after initialization"); + } + @Override public CachedValue supplyFallback(Supplier fallback) { throw new UnsupportedOperationException("Cannot set fallback after initialization"); @@ -105,6 +110,12 @@ public T get() { Preconditions.checkArgument(!holder.isDeleted(), "Cannot get value from a deleted UniqueData instance"); T value = holder.getDataManager().getRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), dataType); if (value == null) { + T refreshed = calculateRefreshedValue(getFallback()); + if (refreshed != null) { + set(refreshed); + return refreshed; + } + return getFallback(); } return value; @@ -123,6 +134,18 @@ public void set(@Nullable T value) { holder.getDataManager().setRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), metadata.expireAfterSeconds(), toSet); } + @Override + public @Nullable T refresh() { + Preconditions.checkArgument(!holder.isDeleted(), "Cannot refresh value on a deleted UniqueData instance"); + T value = holder.getDataManager().getRedis(metadata.holderSchema(), metadata.holderTable(), metadata.identifier(), holder.getIdColumns(), dataType); + if (value == null) { + value = getFallback(); + } + T refreshed = calculateRefreshedValue(value); + set(refreshed); + return refreshed; + } + @Override public String toString() { if (holder.isDeleted()) { diff --git a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java index 0a27919..a9edba7 100644 --- a/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java +++ b/core/src/main/java/net/staticstudios/data/impl/data/ReadOnlyCachedValue.java @@ -1,11 +1,12 @@ package net.staticstudios.data.impl.data; -import com.google.common.base.Supplier; import net.staticstudios.data.CachedValue; import net.staticstudios.data.UniqueData; import net.staticstudios.data.util.*; import org.jetbrains.annotations.Nullable; +import java.util.function.Supplier; + public class ReadOnlyCachedValue extends AbstractCachedValue { private final T value; private final UniqueData holder; @@ -68,6 +69,11 @@ public CachedValue supplyFallback(Supplier fallback) { throw new UnsupportedOperationException("Cannot set fallback on a read-only CachedValue"); } + @Override + public CachedValue refresher(Class clazz, CachedValueRefresher refresher) { + throw new UnsupportedOperationException("Cannot set refresher on a read-only CachedValue"); + } + @Override public T get() { return value; @@ -78,6 +84,11 @@ public void set(T value) { throw new UnsupportedOperationException("Cannot set value on a read-only CachedValue"); } + @Override + public @Nullable T refresh() { + return value; + } + @Override public String toString() { return "ReadOnlyCachedValue{" + diff --git a/core/src/main/java/net/staticstudios/data/util/CachedValueRefresher.java b/core/src/main/java/net/staticstudios/data/util/CachedValueRefresher.java new file mode 100644 index 0000000..4b0d77d --- /dev/null +++ b/core/src/main/java/net/staticstudios/data/util/CachedValueRefresher.java @@ -0,0 +1,8 @@ +package net.staticstudios.data.util; + +import net.staticstudios.data.UniqueData; +import org.jetbrains.annotations.Nullable; + +public interface CachedValueRefresher { + T apply(U holder, @Nullable T previousValue); +} diff --git a/core/src/test/java/net/staticstudios/data/CachedValueTest.java b/core/src/test/java/net/staticstudios/data/CachedValueTest.java index 98cd16f..a582b86 100644 --- a/core/src/test/java/net/staticstudios/data/CachedValueTest.java +++ b/core/src/test/java/net/staticstudios/data/CachedValueTest.java @@ -12,8 +12,7 @@ import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; public class CachedValueTest extends DataTest { @@ -159,4 +158,32 @@ public void testLoadCachedValues() { assertEquals(true, user.onCooldown.get()); assertEquals(5, user.cooldownUpdates.get()); } + + @Test + public void testRefreshCachedValues() throws InterruptedException { + DataManager dataManager = getMockEnvironments().getFirst().dataManager(); + dataManager.load(MockUser.class); + dataManager.finishLoading(); + MockUser user = MockUser.builder(dataManager) + .id(UUID.randomUUID()) + .name("john doe") + .insert(InsertMode.ASYNC); + + + assertEquals(0, user.counter.get()); + assertEquals(1, user.counter.refresh()); + assertEquals(2, user.counter.refresh()); + assertEquals(2, user.counter.get()); + + Thread.sleep(10_000); //wait for the cached value to expire + + String counterKey = RedisUtils.buildRedisKey("public", "users", "counter", user.getIdColumns()); + + Jedis jedis = getJedis(); + assertFalse(jedis.exists(counterKey)); + + assertEquals(2, user.counter.get()); //trigger a refresh + waitForDataPropagation(); + assertEquals("2", gson.fromJson(jedis.get(counterKey), RedisEncodedValue.class).value()); + } } \ No newline at end of file diff --git a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java index 559c938..0541a8a 100644 --- a/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java +++ b/core/src/test/java/net/staticstudios/data/mock/user/MockUser.java @@ -102,6 +102,20 @@ public class MockUser extends UniqueData { .onUpdate(MockUser.class, (user, update) -> user.cooldownUpdates.set(user.cooldownUpdates.get() + 1)) .withFallback(false); + @Column(name = "counter", nullable = true) + public PersistentValue persistentCounter; + + @ExpireAfter(5) + @Identifier("counter") + public CachedValue counter = CachedValue.of(this, Integer.class) + .refresher(MockUser.class, (user, prev) -> { + if (prev == null) { + return user.persistentCounter.get() != null ? user.persistentCounter.get() : 0; + } + return prev + 1; + }) + .onUpdate(MockUser.class, (user, update) -> user.persistentCounter.set(update.newValue())); + public int getNameUpdates() { return nameUpdates.get(); }