-
Notifications
You must be signed in to change notification settings - Fork 0
ValkeyCacheService: pluggable ObjectInputStream for cross-relocation reads #5
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
base: streak-valkey
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |||||||||
| import java.io.ByteArrayInputStream; | ||||||||||
| import java.io.ByteArrayOutputStream; | ||||||||||
| import java.io.IOException; | ||||||||||
| import java.io.InputStream; | ||||||||||
| import java.io.ObjectInputStream; | ||||||||||
| import java.io.ObjectOutputStream; | ||||||||||
| import java.nio.charset.StandardCharsets; | ||||||||||
|
|
@@ -25,6 +26,7 @@ | |||||||||
| import java.util.LinkedHashMap; | ||||||||||
| import java.util.List; | ||||||||||
| import java.util.Map; | ||||||||||
| import java.util.Objects; | ||||||||||
| import java.util.Set; | ||||||||||
| import java.util.concurrent.CompletableFuture; | ||||||||||
| import java.util.concurrent.ExecutionException; | ||||||||||
|
|
@@ -80,6 +82,19 @@ | |||||||||
| */ | ||||||||||
| public class ValkeyCacheService implements MemcacheService { | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Wraps the raw bytes of a stored value in the {@link ObjectInputStream} used to deserialize it. | ||||||||||
| * The default is a plain {@link ObjectInputStream}, but callers can supply a subclass that | ||||||||||
| * overrides {@link ObjectInputStream#resolveClass} — Streak passes one that bridges shaded | ||||||||||
| * ("streak."-relocated) and un-relocated class names, so processes built with different shadowJar | ||||||||||
| * relocations can share one cache. Kept as an explicit interface rather than a | ||||||||||
| * {@code Function<InputStream, ObjectInputStream>} because the constructor throws {@link IOException}. | ||||||||||
| */ | ||||||||||
| @FunctionalInterface | ||||||||||
| public interface ObjectInputStreamFactory { | ||||||||||
| ObjectInputStream create(InputStream in) throws IOException; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** Single-byte sentinel for null. Cannot collide with Java-serialized data (which starts 0xAC 0xED). */ | ||||||||||
| private static final byte[] NULL_VALUE = new byte[]{0}; | ||||||||||
|
|
||||||||||
|
|
@@ -123,17 +138,34 @@ public class ValkeyCacheService implements MemcacheService { | |||||||||
| /** Cold-cache sentinel options: {@code NX} plus the default TTL. Pre-built for the same reason as {@link #defaultSetOptions}. */ | ||||||||||
| private final SetOptions defaultNxSetOptions; | ||||||||||
|
|
||||||||||
| /** Uses {@link #DEFAULT_EXPIRATION_SECONDS} as the fallback TTL. */ | ||||||||||
| /** Builds the {@link ObjectInputStream} used on the read path; see {@link ObjectInputStreamFactory}. */ | ||||||||||
| private final ObjectInputStreamFactory objectInputStreamFactory; | ||||||||||
|
|
||||||||||
| /** Uses {@link #DEFAULT_EXPIRATION_SECONDS} as the fallback TTL and a plain {@link ObjectInputStream}. */ | ||||||||||
| public ValkeyCacheService(final BaseClient client) { | ||||||||||
| this(client, DEFAULT_EXPIRATION_SECONDS); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** Uses {@link #DEFAULT_EXPIRATION_SECONDS} as the fallback TTL. */ | ||||||||||
| public ValkeyCacheService(final BaseClient client, final ObjectInputStreamFactory objectInputStreamFactory) { | ||||||||||
| this(client, DEFAULT_EXPIRATION_SECONDS, objectInputStreamFactory); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| /** Uses a plain {@link ObjectInputStream} on the read path. */ | ||||||||||
| public ValkeyCacheService(final BaseClient client, final int defaultExpirationSeconds) { | ||||||||||
| this(client, defaultExpirationSeconds, ObjectInputStream::new); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public ValkeyCacheService( | ||||||||||
| final BaseClient client, | ||||||||||
| final int defaultExpirationSeconds, | ||||||||||
| final ObjectInputStreamFactory objectInputStreamFactory) { | ||||||||||
| if (defaultExpirationSeconds <= 0) { | ||||||||||
| throw new IllegalArgumentException("defaultExpirationSeconds must be positive, got " + defaultExpirationSeconds); | ||||||||||
| } | ||||||||||
| this.client = client; | ||||||||||
| this.defaultExpirationSeconds = defaultExpirationSeconds; | ||||||||||
| this.objectInputStreamFactory = Objects.requireNonNull(objectInputStreamFactory, "objectInputStreamFactory"); | ||||||||||
| this.defaultSetOptions = SetOptions.builder() | ||||||||||
| .expiry(SetOptions.Expiry.Seconds((long) defaultExpirationSeconds)) | ||||||||||
| .build(); | ||||||||||
|
|
@@ -157,7 +189,7 @@ private static byte[] toCacheBytes(final Object thing) { | |||||||||
| return deflate(serialized); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private static Object fromCacheBytes(final byte[] bytes) { | ||||||||||
| private Object fromCacheBytes(final byte[] bytes) { | ||||||||||
| if (bytes == null || Arrays.equals(bytes, NULL_VALUE)) { | ||||||||||
| return null; | ||||||||||
| } | ||||||||||
|
|
@@ -181,9 +213,9 @@ private static byte[] serialize(final Object thing) { | |||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| private static Object deserialize(final byte[] serialized) { | ||||||||||
| private Object deserialize(final byte[] serialized) { | ||||||||||
| try (final ByteArrayInputStream bais = new ByteArrayInputStream(serialized); | ||||||||||
| final ObjectInputStream ois = new ObjectInputStream(bais)) { | ||||||||||
| final ObjectInputStream ois = objectInputStreamFactory.create(bais)) { | ||||||||||
| return ois.readObject(); | ||||||||||
|
Comment on lines
+218
to
219
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a custom
Suggested change
|
||||||||||
| } catch (final IOException | ClassNotFoundException e) { | ||||||||||
| throw new RuntimeException("Failed to deserialize cache value", e); | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
clientparameter is a required dependency forValkeyCacheServicebut is currently assigned directly without a null check. To prevent potentialNullPointerExceptions later during cache operations, it is recommended to enforce defensive programming by validating thatclientis not null usingObjects.requireNonNull.