Skip to content

ValkeyCacheService: pluggable ObjectInputStream for cross-relocation reads#5

Open
frew wants to merge 2 commits into
streak-valkeyfrom
relocation-aware-object-input-stream
Open

ValkeyCacheService: pluggable ObjectInputStream for cross-relocation reads#5
frew wants to merge 2 commits into
streak-valkeyfrom
relocation-aware-object-input-stream

Conversation

@frew

@frew frew commented Jul 9, 2026

Copy link
Copy Markdown

Why

MailFoo shares one Valkey Objectify entity cache across processes that relocate com.google.* (Guava, protobuf) differently in their shadowJars (app server = un-relocated; ServerSpark/JettyServers = streak.-relocated). ValkeyCacheService.deserialize used a plain ObjectInputStream, and JDK serialization embeds the writer's class names, so a relocated writer's entry fails in an un-relocated reader with:

ClassNotFoundException: streak.com.google.common.collect.ImmutableList$SerializedForm

Change

  • Add an ObjectInputStreamFactory hook; deserialize builds its stream through it. Defaults to a plain ObjectInputStream, so existing callers are unaffected.
  • MailFoo supplies a KeySubstitutingObjectInputStream that bridges shaded ↔ canonical names (in readClassDescriptor(), the only hook allowed to substitute a differently-named class post-JDK-17).
  • New constructors: (client, factory) and (client, ttl, factory).

Tests

  • Added injectedFactoryRemapsClassNamesOnRead (descriptor swap honored on the read path) and factoryConstructorRoundTripsAndAppliesDefaultTtl.
  • Retains all prior behavior (compression, TTL, CAS) — this is -7 plus the factory hook, published as 6.1.4-streak-valkey-8.

Note: the Valkey testcontainers suite requires Docker; test-compile and the new logic were verified locally.

Fred Wulff added 2 commits July 9, 2026 09:23
…reads

Adds an ObjectInputStreamFactory so callers can supply an ObjectInputStream
that bridges shaded ("streak."-relocated) and un-relocated class names. Lets
processes built with different shadowJar relocations share one Valkey cache
without ClassNotFoundException on each other's Java-serialized entries. Defaults
to a plain ObjectInputStream, so existing callers are unaffected.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an ObjectInputStreamFactory functional interface to ValkeyCacheService, allowing callers to supply custom ObjectInputStream implementations (e.g., for class name remapping during deserialization). It adds new constructors to accept this factory, updates the deserialization logic to use it, and includes corresponding unit tests. The feedback suggests adding defensive null checks for the client parameter in the constructor and for the ObjectInputStream returned by the factory to prevent potential NullPointerExceptions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if (defaultExpirationSeconds <= 0) {
throw new IllegalArgumentException("defaultExpirationSeconds must be positive, got " + defaultExpirationSeconds);
}
this.client = client;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The client parameter is a required dependency for ValkeyCacheService but is currently assigned directly without a null check. To prevent potential NullPointerExceptions later during cache operations, it is recommended to enforce defensive programming by validating that client is not null using Objects.requireNonNull.

Suggested change
this.client = client;
this.client = Objects.requireNonNull(client, "client");

Comment on lines +218 to 219
final ObjectInputStream ois = objectInputStreamFactory.create(bais)) {
return ois.readObject();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If a custom ObjectInputStreamFactory implementation incorrectly returns null, calling ois.readObject() will throw a generic NullPointerException. To adhere to defensive programming practices, we should check if the returned ObjectInputStream is null using Objects.requireNonNull.

Suggested change
final ObjectInputStream ois = objectInputStreamFactory.create(bais)) {
return ois.readObject();
final ObjectInputStream ois = Objects.requireNonNull(objectInputStreamFactory.create(bais), "ObjectInputStreamFactory returned null")) {
return ois.readObject();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant