Skip to content

Commit

Permalink
Add support for Redis 7 functions #2185
Browse files Browse the repository at this point in the history
Support for FCALL and FUNCTION commands.
  • Loading branch information
mp911de committed Oct 18, 2023
1 parent 9de4b7f commit a30addf
Show file tree
Hide file tree
Showing 35 changed files with 1,709 additions and 42 deletions.
5 changes: 5 additions & 0 deletions src/main/asciidoc/getting-started.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ include::{ext-doc}/Pub-Sub.asciidoc[leveloffset=+1]
=== Transactions/Multi

include::{ext-doc}/Transactions.asciidoc[leveloffset=+1]

[[scripting-and-functions]]
=== Scripting and Functions

include::scripting-and-functions.asciidoc[]
5 changes: 5 additions & 0 deletions src/main/asciidoc/new-features.adoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
[[new-features]]
= New & Noteworthy

[[new-features.6-3-0]]
== What's new in Lettuce 6.3

* <<_redis_functions,Redis Function support>> (`fcall` and `FUNCTION` commands).

[[new-features.6-2-0]]
== What's new in Lettuce 6.2

Expand Down
4 changes: 4 additions & 0 deletions src/main/asciidoc/scripting-and-functions.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:command-interfaces-link: <<redis-command-interfaces,Command Interfaces>>
[[redis-scripting-and-functions]]
include::{ext-doc}/Scripting-and-Functions.asciidoc[leveloffset=+2]

71 changes: 71 additions & 0 deletions src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,71 @@ public RedisFuture<Long> expiretime(K key) {
return dispatch(commandBuilder.expiretime(key));
}

@Override
public <T> RedisFuture<T> fcall(String function, ScriptOutputType type, K... keys) {
return dispatch(commandBuilder.fcall(function, type, false, keys));
}

@Override
public <T> RedisFuture<T> fcall(String function, ScriptOutputType type, K[] keys, V... values) {
return dispatch(commandBuilder.fcall(function, type, false, keys, values));
}

@Override
public <T> RedisFuture<T> fcallReadOnly(String function, ScriptOutputType type, K... keys) {
return dispatch(commandBuilder.fcall(function, type, true, keys));
}

@Override
public <T> RedisFuture<T> fcallReadOnly(String function, ScriptOutputType type, K[] keys, V... values) {
return dispatch(commandBuilder.fcall(function, type, true, keys, values));
}

@Override
public RedisFuture<String> functionLoad(String functionCode) {
return functionLoad(functionCode, false);
}

@Override
public RedisFuture<String> functionLoad(String functionCode, boolean replace) {
return dispatch(commandBuilder.functionLoad(encodeFunction(functionCode), replace));
}

@Override
public RedisFuture<byte[]> functionDump() {
return dispatch(commandBuilder.functionDump());
}

@Override
public RedisFuture<String> functionRestore(byte[] dump) {
return functionRestore(dump, null);
}

@Override
public RedisFuture<String> functionRestore(byte[] dump, FunctionRestoreMode mode) {
return dispatch(commandBuilder.functionRestore(dump, mode));
}

@Override
public RedisFuture<String> functionFlush(FlushMode flushMode) {
return dispatch(commandBuilder.functionFlush(flushMode));
}

@Override
public RedisFuture<String> functionKill() {
return dispatch(commandBuilder.functionKill());
}

@Override
public RedisFuture<List<Map<String, Object>>> functionList() {
return functionList(null);
}

@Override
public RedisFuture<List<Map<String, Object>>> functionList(String libraryName) {
return dispatch(commandBuilder.functionList(libraryName));
}

@Override
public void flushCommands() {
connection.flushCommands();
Expand Down Expand Up @@ -2906,6 +2971,12 @@ public RedisFuture<Long> zunionstore(K destination, ZStoreArgs zStoreArgs, K...
return dispatch(commandBuilder.zunionstore(destination, zStoreArgs, keys));
}

private byte[] encodeFunction(String functionCode) {
LettuceAssert.notNull(functionCode, "Function code must not be null");
LettuceAssert.notEmpty(functionCode, "Function code script must not be empty");
return functionCode.getBytes(getConnection().getOptions().getScriptCharset());
}

private byte[] encodeScript(String script) {
LettuceAssert.notNull(script, "Lua script must not be null");
LettuceAssert.notEmpty(script, "Lua script must not be empty");
Expand Down
71 changes: 71 additions & 0 deletions src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,71 @@ public Mono<Long> expiretime(K key) {
return createMono(() -> commandBuilder.expiretime(key));
}

@Override
public <T> Flux<T> fcall(String function, ScriptOutputType type, K... keys) {
return createFlux(() -> commandBuilder.fcall(function, type, false, keys));
}

@Override
public <T> Flux<T> fcall(String function, ScriptOutputType type, K[] keys, V... values) {
return createFlux(() -> commandBuilder.fcall(function, type, false, keys, values));
}

@Override
public <T> Flux<T> fcallReadOnly(String function, ScriptOutputType type, K... keys) {
return createFlux(() -> commandBuilder.fcall(function, type, true, keys));
}

@Override
public <T> Flux<T> fcallReadOnly(String function, ScriptOutputType type, K[] keys, V... values) {
return createFlux(() -> commandBuilder.fcall(function, type, true, keys, values));
}

@Override
public Mono<String> functionLoad(String functionCode) {
return functionLoad(functionCode, false);
}

@Override
public Mono<String> functionLoad(String functionCode, boolean replace) {
return createMono(() -> commandBuilder.functionLoad(encodeScript(functionCode), replace));
}

@Override
public Mono<byte[]> functionDump() {
return createMono(commandBuilder::functionDump);
}

@Override
public Mono<String> functionRestore(byte[] dump) {
return createMono(() -> commandBuilder.functionRestore(dump, null));
}

@Override
public Mono<String> functionRestore(byte[] dump, FunctionRestoreMode mode) {
return createMono(() -> commandBuilder.functionRestore(dump, mode));
}

@Override
public Mono<String> functionFlush(FlushMode flushMode) {
return createMono(() -> commandBuilder.functionFlush(flushMode));
}

@Override
public Mono<String> functionKill() {
return createMono(commandBuilder::functionKill);
}

@Override
public Flux<Map<String, Object>> functionList() {
return createDissolvingFlux(() -> commandBuilder.functionList(null));
}

@Override
public Flux<Map<String, Object>> functionList(String libraryName) {
return createDissolvingFlux(() -> commandBuilder.functionList(libraryName));
}

@Override
public void flushCommands() {
connection.flushCommands();
Expand Down Expand Up @@ -2975,6 +3040,12 @@ public Mono<Long> zunionstore(K destination, ZStoreArgs zStoreArgs, K... keys) {
return createMono(() -> commandBuilder.zunionstore(destination, zStoreArgs, keys));
}

private byte[] encodeFunction(String functionCode) {
LettuceAssert.notNull(functionCode, "Function code must not be null");
LettuceAssert.notEmpty(functionCode, "Function code script must not be empty");
return functionCode.getBytes(getConnection().getOptions().getScriptCharset());
}

private byte[] encodeScript(String script) {
LettuceAssert.notNull(script, "Lua script must not be null");
LettuceAssert.notEmpty(script, "Lua script must not be empty");
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/io/lettuce/core/FunctionRestoreMode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.lettuce.core;

import java.nio.charset.StandardCharsets;

import io.lettuce.core.protocol.ProtocolKeyword;

/**
* Restore mode for {@code FUNCTION RESTORE}.
*
* @author Mark Paluch
* @since 6.3
*/
public enum FunctionRestoreMode implements ProtocolKeyword {

/**
* Appends the restored libraries to the existing libraries and aborts on collision. This is the default policy.
*/
APPEND,

/**
* Deletes all existing libraries before restoring the payload.
*/
FLUSH,

/**
* Appends the restored libraries to the existing libraries, replacing any existing ones in case of name collisions. Note
* that this policy doesn't prevent function name collisions, only libraries.
*/
REPLACE;

public final byte[] bytes;

FunctionRestoreMode() {
bytes = name().getBytes(StandardCharsets.US_ASCII);
}

@Override
public byte[] getBytes() {
return bytes;
}

}
67 changes: 67 additions & 0 deletions src/main/java/io/lettuce/core/RedisCommandBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,73 @@ Command<K, V, String> flushdb(FlushMode flushMode) {
return createCommand(FLUSHDB, new StatusOutput<>(codec), new CommandArgs<>(codec).add(flushMode));
}

<T> Command<K, V, T> fcall(String function, ScriptOutputType type, boolean readonly, K[] keys, V... values) {
LettuceAssert.notEmpty(function, "Function " + MUST_NOT_BE_EMPTY);
LettuceAssert.notNull(type, "ScriptOutputType " + MUST_NOT_BE_NULL);
LettuceAssert.notNull(keys, "Keys " + MUST_NOT_BE_NULL);
LettuceAssert.notNull(values, "Values " + MUST_NOT_BE_NULL);

CommandArgs<K, V> args = new CommandArgs<>(codec);
args.add(function).add(keys.length).addKeys(keys).addValues(values);
CommandOutput<K, V, T> output = newScriptOutput(codec, type);

return createCommand(readonly ? FCALL_RO : FCALL, output, args);
}

Command<K, V, String> functionLoad(byte[] functionCode, boolean replace) {
LettuceAssert.notNull(functionCode, "Function code " + MUST_NOT_BE_NULL);

CommandArgs<K, V> args = new CommandArgs<>(codec).add(LOAD);
if (replace) {
args.add(REPLACE);
}
args.add(functionCode);

return createCommand(FUNCTION, new StatusOutput<>(codec), args);
}

Command<K, V, byte[]> functionDump() {
return createCommand(FUNCTION, new ByteArrayOutput<>(codec), new CommandArgs<>(codec).add(DUMP));
}

Command<K, V, String> functionRestore(byte dump[], FunctionRestoreMode mode) {

LettuceAssert.notNull(dump, "Function dump " + MUST_NOT_BE_NULL);
CommandArgs<K, V> args = new CommandArgs<>(codec).add(RESTORE).add(dump);

if (mode != null) {
args.add(mode);
}

return createCommand(FUNCTION, new StatusOutput<>(codec), args);
}

Command<K, V, String> functionFlush(FlushMode mode) {

CommandArgs<K, V> args = new CommandArgs<>(codec).add(FLUSH);

if (mode != null) {
args.add(mode);
}

return createCommand(FUNCTION, new StatusOutput<>(codec), args);
}

Command<K, V, String> functionKill() {
return createCommand(FUNCTION, new StatusOutput<>(codec), new CommandArgs<>(codec).add(KILL));
}

Command<K, V, List<Map<String, Object>>> functionList(String pattern) {

CommandArgs<K, V> args = new CommandArgs<>(codec).add(LIST);

if (pattern != null) {
args.add("LIBRARYNAME").add(pattern);
}

return createCommand(FUNCTION, (CommandOutput) new ObjectOutput<>(StringCodec.UTF8), args);
}

Command<K, V, Long> geoadd(K key, double longitude, double latitude, V member, GeoAddArgs geoArgs) {
notNullKey(key);

Expand Down
34 changes: 33 additions & 1 deletion src/main/java/io/lettuce/core/ScriptOutputType.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.lettuce.core;

import java.nio.ByteBuffer;

/**
* A Lua script returns one of the following types:
*
Expand All @@ -24,6 +26,7 @@
* <li>{@link #STATUS} status string</li>
* <li>{@link #VALUE} value</li>
* <li>{@link #MULTI} of these types</li>
* <li>{@link #OBJECT} result object defined by the RESP3 response</li>
* </ul>
*
* <strong>Redis to Lua</strong> conversion table.
Expand All @@ -49,5 +52,34 @@
* @author Will Glozer
*/
public enum ScriptOutputType {
BOOLEAN, INTEGER, MULTI, STATUS, VALUE

/**
* Boolean output (expects a number {@code 0} or {@code 1} to be converted to a boolean value).
*/
BOOLEAN,

/**
* {@link Long integer} output.
*/
INTEGER,

/**
* List of flat arrays.
*/
MULTI,

/**
* Simple status value such as {@code OK}. The Redis response is parsed as ASCII.
*/
STATUS,

/**
* Value return type decoded through {@link io.lettuce.core.codec.RedisCodec#decodeValue(ByteBuffer)}.
*/
VALUE,

/**
* RESP3-defined object output supporting all Redis response structures.
*/
OBJECT
}
11 changes: 6 additions & 5 deletions src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
* @author Mark Paluch
* @since 3.0
*/
public interface RedisAsyncCommands<K, V> extends BaseRedisAsyncCommands<K, V>, RedisAclAsyncCommands<K, V>, RedisClusterAsyncCommands<K, V>,
RedisGeoAsyncCommands<K, V>, RedisHashAsyncCommands<K, V>, RedisHLLAsyncCommands<K, V>, RedisKeyAsyncCommands<K, V>,
RedisListAsyncCommands<K, V>, RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>,
RedisSetAsyncCommands<K, V>, RedisSortedSetAsyncCommands<K, V>, RedisStreamAsyncCommands<K, V>,
RedisStringAsyncCommands<K, V>, RedisTransactionalAsyncCommands<K, V> {
public interface RedisAsyncCommands<K, V> extends BaseRedisAsyncCommands<K, V>, RedisAclAsyncCommands<K, V>,
RedisClusterAsyncCommands<K, V>, RedisFunctionAsyncCommands<K, V>, RedisGeoAsyncCommands<K, V>,
RedisHashAsyncCommands<K, V>, RedisHLLAsyncCommands<K, V>, RedisKeyAsyncCommands<K, V>, RedisListAsyncCommands<K, V>,
RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>, RedisSetAsyncCommands<K, V>,
RedisSortedSetAsyncCommands<K, V>, RedisStreamAsyncCommands<K, V>, RedisStringAsyncCommands<K, V>,
RedisTransactionalAsyncCommands<K, V> {

/**
* Authenticate to the server.
Expand Down
Loading

0 comments on commit a30addf

Please sign in to comment.