Skip to content

Commit ff1f481

Browse files
committed
client-side caching with a new client impl and connection wrapper
simplify cache impls; some javadocs and tests more javadocs; invalidation handler on client basic support for max age in default impls initial PR feedback; naming consistencies checkpoint on wrapped connection push tracking status down to actual conn impl simplify cache impls to just LRU backed by LinkedHashMap cleanup client wrapper; test simple operation docs update make caching configured via options, not an explicit wrapper regenerate options converters
1 parent cbe76df commit ff1f481

20 files changed

+1595
-8
lines changed

Diff for: src/main/asciidoc/index.adoc

+47-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Redis has a rich API and it can be organized in the following groups:
2424
* Strings - Commands to work with Strings.
2525
* Transactions - Commands to handle transaction lifecycle.
2626
* Streams - Commands to handle streaming.
27+
* Client-side caching - Commands to control client-side caching.
2728
2829
== Using Vert.x-Redis
2930

@@ -182,9 +183,54 @@ And from another place in the code publish messages to the queue:
182183
----
183184

184185
NOTE: It is important to remember that the commands `SUBSCRIBE`, `UNSUBSCRIBE`, `PSUBSCRIBE` and `PUNSUBSCRIBE` are `void`.
185-
This means that the result in case of success is `null` not a instance of response.
186+
This means that the result in case of success is `null` not an instance of response.
186187
All messages are then routed through the handler on the client.
187188

189+
== Client-side Caching
190+
191+
Redis supports client-side caching implementations using a strategy called _Tracking_.
192+
193+
All modes of the client support caching except connections that are in pub/sub mode.
194+
195+
To create a client with client-side caching, one would do:
196+
197+
[source,$lang]
198+
----
199+
{@link examples.RedisExamples#clientCaching1}
200+
----
201+
202+
A specific interface, `CachingRedis`, is exposed for a caching client that allows an invalidation handler to be attached or a flush command to be issued.
203+
The invalidation handler will be invoked with all the keys being invalidated whenever a message is received on the invalidations connection.
204+
205+
To attach an invalidations handler:
206+
207+
[source,$lang]
208+
----
209+
{@link examples.RedisExamples#clientCaching2}
210+
----
211+
212+
To manually flush the client's cache store:
213+
214+
[source,$lang]
215+
----
216+
{@link examples.RedisExamples#clientCaching3}
217+
----
218+
219+
The client comes with a default cache store out of the box, but you can write your own if you prefer.
220+
221+
The implementations are expected to follow the `ServiceLoader` conventions and all stores that are available at runtime from the classpath will be exposed.
222+
When more than 1 implementation is available the first one that can be instantiated and configured with success becomes the default.
223+
If none is available, then the default is a simple Least-Recently-Used (LRU) cache backed by a `LinkedHashMap`.
224+
225+
[source,$lang]
226+
----
227+
{@link examples.RedisExamples#clientCaching4}
228+
----
229+
230+
NOTE: The cache is not a write-through cache. A value will not be stored in the client-side cache until the value is fetched from Redis for the first time.
231+
To avoid write-then-read race conditions within the same batch, read commands that are part of a batch will not check the cache first.
232+
Additionally, the current implementation does not support the `OPTIN` or `NOLOOP` options.
233+
188234
== Tracing commands
189235

190236
The Redis client can trace command execution when Vert.x has tracing enabled.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.vertx.redis.client;
2+
3+
import io.vertx.core.json.JsonObject;
4+
import io.vertx.core.json.JsonArray;
5+
import io.vertx.core.json.impl.JsonUtil;
6+
import java.time.Instant;
7+
import java.time.format.DateTimeFormatter;
8+
import java.util.Base64;
9+
10+
/**
11+
* Converter and mapper for {@link io.vertx.redis.client.CachingRedisOptions}.
12+
* NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.CachingRedisOptions} original class using Vert.x codegen.
13+
*/
14+
public class CachingRedisOptionsConverter {
15+
16+
17+
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
18+
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
19+
20+
public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, CachingRedisOptions obj) {
21+
for (java.util.Map.Entry<String, Object> member : json) {
22+
switch (member.getKey()) {
23+
case "enabled":
24+
if (member.getValue() instanceof Boolean) {
25+
obj.setEnabled((Boolean)member.getValue());
26+
}
27+
break;
28+
case "maxCacheSize":
29+
if (member.getValue() instanceof Number) {
30+
obj.setMaxCacheSize(((Number)member.getValue()).intValue());
31+
}
32+
break;
33+
case "maxAge":
34+
if (member.getValue() instanceof Number) {
35+
obj.setMaxAge(((Number)member.getValue()).longValue());
36+
}
37+
break;
38+
case "maxAgeUnit":
39+
if (member.getValue() instanceof String) {
40+
obj.setMaxAgeUnit(java.util.concurrent.TimeUnit.valueOf((String)member.getValue()));
41+
}
42+
break;
43+
case "mode":
44+
if (member.getValue() instanceof String) {
45+
obj.setMode(io.vertx.redis.client.ClientSideCacheMode.valueOf((String)member.getValue()));
46+
}
47+
break;
48+
case "prefixes":
49+
if (member.getValue() instanceof JsonArray) {
50+
java.util.ArrayList<java.lang.String> list = new java.util.ArrayList<>();
51+
((Iterable<Object>)member.getValue()).forEach( item -> {
52+
if (item instanceof String)
53+
list.add((String)item);
54+
});
55+
obj.setPrefixes(list);
56+
}
57+
break;
58+
case "prefix":
59+
if (member.getValue() instanceof String) {
60+
obj.setPrefix((String)member.getValue());
61+
}
62+
break;
63+
case "prefixs":
64+
if (member.getValue() instanceof JsonArray) {
65+
((Iterable<Object>)member.getValue()).forEach( item -> {
66+
if (item instanceof String)
67+
obj.addPrefix((String)item);
68+
});
69+
}
70+
break;
71+
}
72+
}
73+
}
74+
75+
public static void toJson(CachingRedisOptions obj, JsonObject json) {
76+
toJson(obj, json.getMap());
77+
}
78+
79+
public static void toJson(CachingRedisOptions obj, java.util.Map<String, Object> json) {
80+
json.put("enabled", obj.getEnabled());
81+
json.put("maxCacheSize", obj.getMaxCacheSize());
82+
json.put("maxAge", obj.getMaxAge());
83+
if (obj.getMaxAgeUnit() != null) {
84+
json.put("maxAgeUnit", obj.getMaxAgeUnit().name());
85+
}
86+
if (obj.getMode() != null) {
87+
json.put("mode", obj.getMode().name());
88+
}
89+
if (obj.getPrefixes() != null) {
90+
JsonArray array = new JsonArray();
91+
obj.getPrefixes().forEach(item -> array.add(item));
92+
json.put("prefixes", array);
93+
}
94+
}
95+
}

Diff for: src/main/generated/io/vertx/redis/client/RedisOptionsConverter.java

+64
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,56 @@ public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json,
123123
obj.setPoolName((String)member.getValue());
124124
}
125125
break;
126+
case "cachingOptions":
127+
break;
128+
case "cacheEnabled":
129+
if (member.getValue() instanceof Boolean) {
130+
obj.setCacheEnabled((Boolean)member.getValue());
131+
}
132+
break;
133+
case "cacheMaxSize":
134+
if (member.getValue() instanceof Number) {
135+
obj.setCacheMaxSize(((Number)member.getValue()).intValue());
136+
}
137+
break;
138+
case "cacheMaxAge":
139+
if (member.getValue() instanceof Number) {
140+
obj.setCacheMaxAge(((Number)member.getValue()).longValue());
141+
}
142+
break;
143+
case "cacheMaxAgeUnit":
144+
if (member.getValue() instanceof String) {
145+
obj.setCacheMaxAgeUnit(java.util.concurrent.TimeUnit.valueOf((String)member.getValue()));
146+
}
147+
break;
148+
case "cacheMode":
149+
if (member.getValue() instanceof String) {
150+
obj.setCacheMode(io.vertx.redis.client.ClientSideCacheMode.valueOf((String)member.getValue()));
151+
}
152+
break;
153+
case "cachePrefixes":
154+
if (member.getValue() instanceof JsonArray) {
155+
java.util.ArrayList<java.lang.String> list = new java.util.ArrayList<>();
156+
((Iterable<Object>)member.getValue()).forEach( item -> {
157+
if (item instanceof String)
158+
list.add((String)item);
159+
});
160+
obj.setCachePrefixes(list);
161+
}
162+
break;
163+
case "cachePrefix":
164+
if (member.getValue() instanceof String) {
165+
obj.setCachePrefix((String)member.getValue());
166+
}
167+
break;
168+
case "cachePrefixs":
169+
if (member.getValue() instanceof JsonArray) {
170+
((Iterable<Object>)member.getValue()).forEach( item -> {
171+
if (item instanceof String)
172+
obj.addCachePrefix((String)item);
173+
});
174+
}
175+
break;
126176
}
127177
}
128178
}
@@ -171,5 +221,19 @@ public static void toJson(RedisOptions obj, java.util.Map<String, Object> json)
171221
if (obj.getPoolName() != null) {
172222
json.put("poolName", obj.getPoolName());
173223
}
224+
json.put("cacheEnabled", obj.getCacheEnabled());
225+
json.put("cacheMaxSize", obj.getCacheMaxSize());
226+
json.put("cacheMaxAge", obj.getCacheMaxAge());
227+
if (obj.getCacheMaxAgeUnit() != null) {
228+
json.put("cacheMaxAgeUnit", obj.getCacheMaxAgeUnit().name());
229+
}
230+
if (obj.getCacheMode() != null) {
231+
json.put("cacheMode", obj.getCacheMode().name());
232+
}
233+
if (obj.getCachePrefixes() != null) {
234+
JsonArray array = new JsonArray();
235+
obj.getCachePrefixes().forEach(item -> array.add(item));
236+
json.put("cachePrefixes", array);
237+
}
174238
}
175239
}

Diff for: src/main/java/examples/RedisExamples.java

+87-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package examples;
22

3+
import io.vertx.codegen.annotations.Nullable;
34
import io.vertx.core.AbstractVerticle;
45
import io.vertx.core.Future;
56
import io.vertx.core.Promise;
67
import io.vertx.core.Vertx;
78
import io.vertx.core.tracing.TracingPolicy;
89
import io.vertx.redis.client.*;
910

11+
import io.vertx.redis.client.impl.CachingRedis;
12+
import io.vertx.redis.client.impl.CachingRedisClient;
13+
import io.vertx.redis.client.impl.RedisClient;
14+
import io.vertx.redis.client.impl.cache.CacheKey;
15+
import io.vertx.redis.client.spi.RedisClientCache;
16+
import java.util.HashMap;
17+
import java.util.Map;
1018
import java.util.concurrent.atomic.AtomicBoolean;
1119

1220
/**
@@ -143,7 +151,7 @@ private Future<RedisConnection> createRedisClient() {
143151

144152
// make sure to invalidate old connection if present
145153
if (redis != null) {
146-
redis.close();;
154+
redis.close();
147155
}
148156

149157
if (CONNECTING.compareAndSet(false, true)) {
@@ -246,4 +254,82 @@ public void example13(Vertx vertx) {
246254
public void tracing1(RedisOptions options) {
247255
options.setTracingPolicy(TracingPolicy.ALWAYS);
248256
}
257+
258+
public void clientCaching1(Vertx vertx) {
259+
Redis.createClient(
260+
vertx,
261+
new RedisOptions()
262+
.setCacheEnabled(true)
263+
.setCacheMaxSize(256)
264+
.setCacheMaxAge(60_000))
265+
.connect()
266+
.onSuccess(conn -> {
267+
// get the value for a key, returning from a local in-memory cache if
268+
// it exists, or fetching from Redis if not. if the value is fetched from
269+
// Redis, it will be stored in the local cache
270+
conn.send(Request.cmd(Command.GET).arg("key"));
271+
});
272+
}
273+
274+
public void clientCaching2(Redis redis) {
275+
CachingRedis cachingClient = (CachingRedis) redis;
276+
277+
cachingClient.invalidationHandler(keys -> {
278+
// something...
279+
});
280+
}
281+
282+
public void clientCaching3(Redis redis) {
283+
CachingRedis cachingClient = (CachingRedis) redis;
284+
285+
cachingClient.flush().onSuccess(ignored -> {
286+
// Success!
287+
});
288+
}
289+
290+
public void clientCaching4(Vertx vertx, RedisClientCache customCache) {
291+
292+
// Register this class in META-INF/services/io.vertx.redis.client.spi.RedisClientCache
293+
class CustomCache implements RedisClientCache {
294+
295+
private final Map<CacheKey, Response> store = new HashMap<>();
296+
297+
@Override
298+
public @Nullable Response get(CacheKey key) {
299+
return store.get(key);
300+
}
301+
302+
@Override
303+
public void put(CacheKey key, Response value) {
304+
store.put(key, value);
305+
}
306+
307+
@Override
308+
public void delete(CacheKey key) {
309+
store.remove(key);
310+
}
311+
312+
@Override
313+
public void flush() {
314+
store.clear();
315+
}
316+
317+
@Override
318+
public void close() {
319+
// Nothing to do here
320+
}
321+
}
322+
323+
Redis.createClient(
324+
vertx,
325+
new RedisOptions()
326+
.setCacheEnabled(true))
327+
.connect()
328+
.onSuccess(conn -> {
329+
// get the value for a key, returning from the custom cache if
330+
// it exists, or fetching from Redis if not. if the value is fetched from
331+
// Redis, it will be stored in the local cache
332+
conn.send(Request.cmd(Command.GET).arg("key"));
333+
});
334+
}
249335
}

0 commit comments

Comments
 (0)