Redis Client Lettuce 5 GA released

After a 13 months development phase and 208 solved tickets, it is my pleasure to announce general availability of Lettuce 5.0. This is a major release coming with several breaking changes and new interesting features and Java 9 compatibility.

Get the release from Maven Central

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.0.0.RELEASE</version>
</dependency>

or download the release bundle from GitHub.

Lettuce 5 introduces the dynamic Redis Commands API. This programming model enables you to declare command methods and invoke commands to your needs and support Redis Modules without waiting for Lettuce to support new commands.

Lettuce defaults to native transport (epoll, kqueue) on Linux respective macOS systems if the native dependency is available.
Lettuce 5 comes with breaking changes; it removes deprecated interfaces RedisConnection and RedisAsyncConnection and their segregated interfaces in favor of StatefulRedisConnection and RedisCommands et al.

Major breaking changes:

  1. We moved the artifact coordinates from biz.paluch.redis:lettuce to io.lettuce:lettuce-core
  2. We relocated packages from biz.paluch.redis to io.lettuce.core. The migration path is straight-forward by replacing the old package name in your imports with the new package name.
  3. The documentation has moved from http://redis.paluch.biz to https://lettuce.io.
  4. Removed Guava.
  5. We removed some deprecated methods, see below for full details.

Lettuce requires only netty 4.1 (netty 4.0 is no longer supported) and Project Reactor 3.1 which brings us to the next change:

The reactive API is based on Reactive Streams by using Project Reactor types Mono and Flux instead of RxJava 1 and Observable.
If you require RxJava’s Single and Observable in your code, then use publisher adapters in rxjava-reactive-streams to adapt Mono and Flux.

This release introduces a new reference guide that is shipped along the regular artifacts.
The reference guide is bound to a particular version and does not change over time, such as the Wiki.

<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>5.0.0.RELEASE</version>
</dependency>

You can find the full change log, containing all changes since the first 5.0 milestone release,
on GitHub. Watch out for BREAKING changes.

Thanks to all contributors that made Lettuce 5 possible. Any feedback is appreciated or file an issue on GitHub.

Dynamic Redis Commands API

The Redis Command Interface abstraction provides a dynamic way for typesafe Redis command invocation. It allows you to declare an interface with command methods to significantly reduce boilerplate code required to invoke a Redis command.

Redis is a data store supporting over 190 documented commands and over 450 command permutations. Command growth and keeping track with upcoming modules are challenging for client developers and Redis user as there is no full command coverage for each module in a single Redis client.

Invoking a custom command with Lettuce requires several lines of code to define command structures pass in arguments and specify the return type.

RedisCodec<String, String> codec = StringCodec.UTF8;
RedisCommands<String, String> commands = ...

String response = redis.dispatch(CommandType.SET, new StatusOutput<>(codec),
                new CommandArgs<>(codec)
                       .addKey(key)
                       .addValue(value));

The central interface in Lettuce Command Interface abstraction is Commands.

This interface acts primarily as a marker interface to help you to discover interfaces that extend this one. You can declare your own command interfaces and argument sequences where the command name is derived from the method name or provided with @Command. Introduction of new commands does not require you to wait for a new Lettuce release but they can invoke commands through own declaration.
That interface could be also supporting different key and value types, depending on the use-case.

Commands are executed synchronously, asynchronous or with a reactive execution model, depending on the method declaration.

public interface MyRedisCommands extends Commands {

    String get(String key); // Synchronous Execution of GET

    @Command("GET")
    byte[] getAsBytes(String key); // Synchronous Execution of GET returning data as byte array

    @Command("SET") // synchronous execution applying a Timeout
    String setSync(String key, String value, Timeout timeout);

    Future<String> set(String key, String value); // asynchronous SET execution

    @Command("SET")
    Mono<String> setReactive(String key, String value); // reactive SET execution using SetArgs

    @CommandNaming(split = DOT) // support for Redis Module command notation -> NR.RUN
    double nrRun(String key, int... indexes);
}

RedisCommandFactory factory = new RedisCommandFactory(connection);

MyRedisCommands commands = factory.getCommands(MyRedisCommands.class);

String value = commands.get("key");

You get a whole lot new possibilities with Redis Command Interfaces. One of them is transparent reactive type adoption. Lettuce’s reactive API is based on Reactive Streams, however with command interfaces you can declare a RxJava 1 or RxJava 2 return type and Lettuce will handle the adoption for you. RxJava 1 users have a migration path that allows using native types without
further conversion.

See also: https://lettuce.io/core/5.0.0.RELEASE/reference/#redis-command-interfaces

Command Interface Batching

Command interfaces support command batching to collect multiple commands in a batch queue and flush the batch in a single write to the transport. Command batching executes commands in a deferred nature. This means that at the time of invocation no result is available. Batching can be only used with synchronous methods without a return value (void) or asynchronous methods returning a RedisFuture.

Command batching can be enabled on two levels:

  • On class level by annotating the command interface with @BatchSize. All methods participate in command batching.
  • On method level by adding CommandBatching to the arguments. Method participates selectively in command batching.
@BatchSize(50)
interface StringCommands extends Commands {

    void set(String key, String value);

    RedisFuture<String> get(String key);

    RedisFuture<String> get(String key, CommandBatching batching);
}

StringCommands commands = …

commands.set("key", "value"); // queued until 50 command invocations reached.
                              // The 50th invocation flushes the queue.

commands.get("key", CommandBatching.queue()); // invocation-level queueing control
commands.get("key", CommandBatching.flush()); // invocation-level queueing control,
                                              // flushes all queued commands

Read more: https://lettuce.io/core/5.0.0.RELEASE/reference/#command-interfaces.batch

Migration to Reactive Streams

Lettuce 4.0 introduced a reactive API based on RxJava 1 and Observable. This was the beginning of reactive Redis support. Lettuce used Observable all over the place as other reactive types like Single and Completable were still beta or in development.

Since that time, a lot changed in the reactive space. RxJava 2 is the successor of RxJava 1 which has now reached end of life. RxJava 2 is not entirely based on Reactive Streams and baselines to Java 6 while other composition libraries can benefit from a Java 8.

This also means, no null values and usage of dedicated value types to express value multiplicity (0|1 and 0|1|N) on the API.

With Lettuce 5.0, the reactive API uses Project Reactor with its Mono and Flux types.

Lettuce 4

Observable<Long> del(K... keys);

Observable<K> keys(K pattern);

Observable<V> mget(K... keys);

Lettuce 5

Mono<Long> del(K... keys);

Flux<K> keys(K pattern);

Flux<KeyValue<K, V>> mget(K... keys);

Switching from RxJava 1 to Project Reactor use requires switching the library. Most operators use similar or even same names. If you’re required to stick to RxJava 1, the use rxjava-reactive-streams to adopt reactive types (RxJava 1 <-> Reactive Streams).

Migrating to Reactive Streams requires value wrapping to indicate absence of values. You will find differences in comparison to the previous API and to the sync/async API in cases where commands can return null values. Lettuce 5.0 comes with new Value types that are monads encapsulating a value (or their absence).

See also: https://lettuce.io/core/5.0.0.RELEASE/reference/#reactive-api

Value, KeyValue, and other value types

The reactive story facilitates immutable types so this release enhances existing value types and introduces new types to reduce null usage and facilitate functional programming.

Value types are based on Value and KeyValue/ScoredValue extend from there. Value is a wrapper type encapsulating a value or its absence. A Value can be created in different ways:

Value<String> value = Value.from(Optional.of("hello"));

Value<String> value = Value.fromNullable(null);

Value<String> value = Value.just("hello");

KeyValue<Long, String> value = KeyValue.from(1L, Optional.of("hello"));
 
KeyValue<String, String> value = KeyValue.just("key", "hello");

It transforms to Optional and Stream to integrate with other functional uses and allows value mapping.

Value.just("hello").stream().filter(…).count();

KeyValue.just("hello").optional().isPresent();

Value.from(Optional.of("hello")).map(s -> s + "-world").getValue();

ScoredValue.just(42, "hello").mapScore(number -> number.doubleValue() * 3.14d).getScore();

You will also find that all public fields of value types are encapsulated with getters and these fields are no longer accessible.

Backoff/Delay strategies

Thanks to @jongyeol

When running cloud-based services with a multitude of services that use Redis, then network partitions impact Redis server connection heavily once the partition ends. A network partition impacts all disconnected applications at the same time and all nodes start reconnecting more or less at the same time.

As soon as the partition ends, the majority of applications reconnect at the same time. Jitter backoff strategies leverage the impact as the time of reconnecting is randomized.

Lettuce comes with various backoff implementations:

  • Equal Jitter
  • Full Jitter
  • Decorrelated Jitter

These are configured in ClientResources:

DefaultClientResources.builder()
        .reconnectDelay(Delay.decorrelatedJitter())
        .build();

DefaultClientResources.builder()
        .reconnectDelay(Delay.equalJitter())
        .build();

See also: https://www.awsarchitectureblog.com/2015/03/backoff.html and
https://lettuce.io/core/5.0.0.RELEASE/reference/#clientresources.advanced-settings

New API for Z…RANGE commands

Sorted Sets range commands come with a streamlined API regarding method overloads. Commands like ZRANGEBYSCORE, ZRANGEBYLEX, ZREMRANGEBYLEX and several others now declare methods accepting Range and Limit objects instead of an growing parameter list. The new Range allows score and value types applying the proper binary encoding.

4.2 and earlier

commands.zcount(key, 1.0, 3.0)

commands.zrangebyscore(key, "-inf", "+inf")

commands.zrangebyscoreWithScores(key, "[1.0", "(4.0")

commands.zrangebyscoreWithScores(key, "-inf", "+inf", 2, 2)

Since 5.0

commands.zcount(key, Range.create(1.0, 3.0));

commands.zrangebyscore(key, Range.unbounded());

commands.zrangebyscoreWithScores(key, Range.from(Boundary.including(1.0), Boundary.excluding(4.0));

commands.zrangebyscoreWithScores(key, Range.unbounded(), Limit.create(2, 2));

Good bye, Guava

Lettuce 5.0 no longer uses Google’s Guava library. Guava was a good friend back in the Java 6-compatible days where Future synchronization and callbacks were no fun to use. That changed with Java 8 and CompletableFuture.

Other uses like HostAndPort or LoadingCache could be either inlined or replaced by Java 8’s Collection framework.

Removal of deprecated interfaces and methods

This release removes deprecated interfaces RedisConnection and RedisAsyncConnection and their segregated interfaces in favor of StatefulRedisConnection and RedisCommands.

You will notice slight differences when using that API. Transactional commands and database selection are no longer available through the Redis Cluster API as the old API was derived from the standalone API. RedisCommands and RedisAsyncCommands are no longer Closeable. Please use commands.getStatefulConnection().close() to close a connection. This change removes ambiguity over closing the commands interface over closing the connection.

Connection pooling replacement

It took quite a while but 4.3 deprecated Lettuce’s existing connection pooling support. That are in particular RedisClient.pool(…) and RedisClient.asyncPool(…). These methods are removed with Lettuce 5.0.

Connection pooling had very limited support and would require additional overloads that clutter the API to expose pooling for all supported connections. This release brings a replacement, that is customizable and does not pollute the API. ConnectionPoolSupport provides methods to create a connection pool accepting a factory method and pool configuration.

Returned connection objects are proxies that return the connection to its pool when calling close(). StatefulConnection implement Closeable to allow usage of try-with-resources.

GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport
        .createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig());


try(StatefulRedisConnection<String, String> connection = pool.borrowObject()) {
    // Work
}


pool.close();

Redis Cluster topology refresh consensus

Cluster topology refreshing can lead in some cases (dynamic topology sources) to orphaning. This can happen if a cluster node is removed from the cluster and lettuce decides to accept the topology view of that removed node. Lettuce gets stuck with that node and is not able to use the remaining cluster.

This release introduces PartitionsConsensus strategies to determine the most appropriate topology view if multiple views are acquired. The strategy can be customized by overriding RedisClusterClient.determinePartitions(Partitions, Map<RedisURI, Partitions>).

Lettuce defaults choosing the topology view with the majority of previously known cluster nodes. This helps Lettuce to stick with the cluster that consists of the most nodes.

See also: https://github.com/lettuce-io/lettuce-core/issues/355

Asynchronous Connections in Redis Cluster

RedisClusterClient now connects asynchronously without intermediate blocking to cluster nodes. The connection progress is shared between
multiple threads that request a cluster node connection for the first time. Previously, connection was sequential-synchronous. Each connection attempt blocked subsequent attempts from other threads. If a cluster node connection ran into a timeout, threads were penalized with a growing wait time. If, say, 10 threads waited for a connection, the last thread had to wait up to 10 times the connection timeout.

Asynchronous connection initiates the connection once using internally a Future so multiple concurrent connection attempts share the resulting Future. Errors fail now faster and cluster node usage is fully asynchronous without synchronization and without the danger to run into a threading deadlock.

Redis Cluster Pub/Sub on Node-Selections

RedisClusterClient.connectPubSub() now returns a StatefulRedisClusterPubSubConnection that allows registration of RedisClusterPubSubListeners and subscription on particular cluster nodes.

Cluster node-specific subscriptions allow usage of keyspace notifications. Keyspace notifications are different from userspace Pub/Sub since keyspace notifications don’t broadcast to the entire cluster but are published only on the node the notification happens. A common usecase is when a key expires in the cluster.

StatefulRedisClusterPubSubConnection connection = client.connectPubSub();

connection.addListener(…);

connection.setNodeMessagePropagation(true);

RedisClusterPubSubCommands<String, String> sync = connection.sync();
sync.slaves().commands().psubscribe("__key*__:expire");

Native Transports

Lettuce now uses native transports by default, if the operating system is qualified and dependencies are available. Lettuce supports epoll (on Linux-based systems) since 4.0 and since this version kqueue (BSD-based systems, like macOS).

Epoll usage can be disabled with system properties by setting io.lettuce.core.epoll=false. In a similar way, kqueue can be disabled
with io.lettuce.core.kqueue=false.

Epoll dependency:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-epoll</artifactId>
    <version>${netty-version}</version>
    <classifier>linux-x86_64</classifier>
</dependency>

Kqueue dependency:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport-native-kqueue</artifactId>
    <version>${netty-version}</version>
    <classifier>osx-x86_64</classifier>
</dependency>

You may also enjoy…