From 666faba3ba1013b47b1e7403d4a48ff35b76ad9e Mon Sep 17 00:00:00 2001 From: Nikita Koksharov Date: Tue, 15 Mar 2022 13:01:17 +0300 Subject: [PATCH] Feature - RFunction object added. #4117 --- .../src/main/java/org/redisson/Redisson.java | 10 + .../java/org/redisson/RedissonFuction.java | 238 ++++++++++++++++++ .../java/org/redisson/RedissonReactive.java | 10 + .../main/java/org/redisson/RedissonRx.java | 10 + .../org/redisson/api/FunctionLibrary.java | 89 +++++++ .../java/org/redisson/api/FunctionStats.java | 125 +++++++++ .../main/java/org/redisson/api/RFunction.java | 227 +++++++++++++++++ .../java/org/redisson/api/RFunctionAsync.java | 164 ++++++++++++ .../org/redisson/api/RFunctionReactive.java | 163 ++++++++++++ .../java/org/redisson/api/RFunctionRx.java | 165 ++++++++++++ .../java/org/redisson/api/RedissonClient.java | 15 ++ .../redisson/api/RedissonReactiveClient.java | 15 ++ .../org/redisson/api/RedissonRxClient.java | 15 ++ .../client/protocol/RedisCommands.java | 87 ++++++- .../command/CommandAsyncExecutor.java | 2 + .../redisson/command/CommandAsyncService.java | 12 +- .../org/redisson/RedissonFunctionTest.java | 99 ++++++++ 17 files changed, 1440 insertions(+), 6 deletions(-) create mode 100644 redisson/src/main/java/org/redisson/RedissonFuction.java create mode 100644 redisson/src/main/java/org/redisson/api/FunctionLibrary.java create mode 100644 redisson/src/main/java/org/redisson/api/FunctionStats.java create mode 100644 redisson/src/main/java/org/redisson/api/RFunction.java create mode 100644 redisson/src/main/java/org/redisson/api/RFunctionAsync.java create mode 100644 redisson/src/main/java/org/redisson/api/RFunctionReactive.java create mode 100644 redisson/src/main/java/org/redisson/api/RFunctionRx.java create mode 100644 redisson/src/test/java/org/redisson/RedissonFunctionTest.java diff --git a/redisson/src/main/java/org/redisson/Redisson.java b/redisson/src/main/java/org/redisson/Redisson.java index 379f62cd1..43c47b939 100755 --- a/redisson/src/main/java/org/redisson/Redisson.java +++ b/redisson/src/main/java/org/redisson/Redisson.java @@ -381,6 +381,16 @@ public class Redisson implements RedissonClient { return new RedissonSet(codec, commandExecutor, name, this); } + @Override + public RFunction getFunction() { + return new RedissonFuction(commandExecutor); + } + + @Override + public RFunction getFunction(Codec codec) { + return new RedissonFuction(commandExecutor, codec); + } + @Override public RScript getScript() { return new RedissonScript(commandExecutor); diff --git a/redisson/src/main/java/org/redisson/RedissonFuction.java b/redisson/src/main/java/org/redisson/RedissonFuction.java new file mode 100644 index 000000000..08682cf02 --- /dev/null +++ b/redisson/src/main/java/org/redisson/RedissonFuction.java @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson; + +import org.redisson.api.FunctionLibrary; +import org.redisson.api.FunctionStats; +import org.redisson.api.RFunction; +import org.redisson.api.RFuture; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.command.CommandAsyncExecutor; +import org.redisson.misc.CompletableFutureWrapper; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonFuction implements RFunction { + + private final Codec codec; + private final CommandAsyncExecutor commandExecutor; + + public RedissonFuction(CommandAsyncExecutor commandExecutor) { + this.commandExecutor = commandExecutor; + this.codec = commandExecutor.getConnectionManager().getCodec(); + } + + public RedissonFuction(CommandAsyncExecutor commandExecutor, Codec codec) { + this.commandExecutor = commandExecutor; + this.codec = codec; + } + + @Override + public void delete(String libraryName) { + commandExecutor.get(deleteAsync(libraryName)); + } + + @Override + public RFuture deleteAsync(String libraryName) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_DELETE, libraryName); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public byte[] dump() { + return commandExecutor.get(dumpAsync()); + } + + @Override + public RFuture dumpAsync() { + return commandExecutor.readAsync((String) null, ByteArrayCodec.INSTANCE, RedisCommands.FUNCTION_DUMP); + } + + @Override + public void flush() { + commandExecutor.get(flushAsync()); + } + + @Override + public RFuture flushAsync() { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_FLUSH); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public void kill() { + commandExecutor.get(killAsync()); + } + + @Override + public RFuture killAsync() { + List> futures = commandExecutor.executeAll(RedisCommands.FUNCTION_KILL); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public List list() { + return commandExecutor.get(listAsync()); + } + + @Override + public RFuture> listAsync() { + return commandExecutor.readAsync((String) null, ByteArrayCodec.INSTANCE, RedisCommands.FUNCTION_LIST); + } + + @Override + public List list(String namePattern) { + return commandExecutor.get(listAsync(namePattern)); + } + + + @Override + public RFuture> listAsync(String namePattern) { + return commandExecutor.readAsync((String) null, ByteArrayCodec.INSTANCE, RedisCommands.FUNCTION_LIST, "LIBRARYNAME", namePattern); + } + + @Override + public void load(String libraryName, String code) { + commandExecutor.get(loadAsync(libraryName, code)); + } + + @Override + public RFuture loadAsync(String libraryName, String code) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_LOAD, "Lua", libraryName, code); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public void loadAndReplace(String libraryName, String code) { + commandExecutor.get(loadAndReplaceAsync(libraryName, code)); + } + + @Override + public RFuture loadAndReplaceAsync(String libraryName, String code) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_LOAD, + "Lua", libraryName, "REPLACE", code); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public void restore(byte[] payload) { + commandExecutor.get(restoreAsync(payload)); + } + + @Override + public RFuture restoreAsync(byte[] payload) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_RESTORE, (Object) payload); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public void restoreAndReplace(byte[] payload) { + commandExecutor.get(restoreAndReplaceAsync(payload)); + } + + @Override + public RFuture restoreAndReplaceAsync(byte[] payload) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_RESTORE, payload, "REPLACE"); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + @Override + public void restoreAfterFlush(byte[] payload) { + commandExecutor.get(restoreAfterFlushAsync(payload)); + } + + @Override + public RFuture restoreAfterFlushAsync(byte[] payload) { + List> futures = commandExecutor.executeMasters(RedisCommands.FUNCTION_RESTORE, payload, "FLUSH"); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + return new CompletableFutureWrapper<>(f); + } + + private List encode(Collection values, Codec codec) { + List result = new ArrayList(values.size()); + for (Object object : values) { + result.add(commandExecutor.encode(codec, object)); + } + return result; + } + + @Override + public R call(String key, Mode mode, String name, ReturnType returnType, List keys, Object... values) { + return commandExecutor.get(callAsync(key, mode, name, returnType, keys, values)); + } + + @Override + public R call(Mode mode, String name, ReturnType returnType, List keys, Object... values) { + return commandExecutor.get(callAsync(mode, name, returnType, keys, values)); + } + + @Override + public R call(Mode mode, String name, ReturnType returnType) { + return commandExecutor.get(callAsync(mode, name, returnType)); + } + + @Override + public FunctionStats stats() { + return commandExecutor.get(statsAsync()); + } + + @Override + public RFuture statsAsync() { + return commandExecutor.readAsync((String) null, StringCodec.INSTANCE, RedisCommands.FUNCTION_STATS); + } + + @Override + public RFuture callAsync(String key, Mode mode, String name, ReturnType returnType, List keys, Object... values) { + List args = new ArrayList<>(); + args.add(name); + args.add(keys.size()); + if (keys.size() > 0) { + args.add(keys); + } + args.addAll(encode(Arrays.asList(values), codec)); + if (mode == Mode.READ) { + return commandExecutor.readAsync(key, codec, returnType.getCommand(), args.toArray()); + } + return commandExecutor.writeAsync(key, codec, returnType.getCommand(), args.toArray()); + } + + @Override + public RFuture callAsync(Mode mode, String name, ReturnType returnType, List keys, Object... values) { + return callAsync(null, mode, name, returnType, keys, values); + } + + @Override + public RFuture callAsync(Mode mode, String name, ReturnType returnType) { + return callAsync(mode, name, returnType, Collections.emptyList()); + + } +} diff --git a/redisson/src/main/java/org/redisson/RedissonReactive.java b/redisson/src/main/java/org/redisson/RedissonReactive.java index 9732aeadd..6dff4e102 100644 --- a/redisson/src/main/java/org/redisson/RedissonReactive.java +++ b/redisson/src/main/java/org/redisson/RedissonReactive.java @@ -497,6 +497,16 @@ public class RedissonReactive implements RedissonReactiveClient { return ReactiveProxyBuilder.create(commandExecutor, new RedissonBitSet(commandExecutor, name), RBitSetReactive.class); } + @Override + public RFunctionReactive getFunction() { + return ReactiveProxyBuilder.create(commandExecutor, new RedissonFuction(commandExecutor), RFunctionReactive.class); + } + + @Override + public RFunctionReactive getFunction(Codec codec) { + return ReactiveProxyBuilder.create(commandExecutor, new RedissonFuction(commandExecutor, codec), RFunctionReactive.class); + } + @Override public RScriptReactive getScript() { return ReactiveProxyBuilder.create(commandExecutor, new RedissonScript(commandExecutor), RScriptReactive.class); diff --git a/redisson/src/main/java/org/redisson/RedissonRx.java b/redisson/src/main/java/org/redisson/RedissonRx.java index 91f3ef04d..80c8abaf9 100644 --- a/redisson/src/main/java/org/redisson/RedissonRx.java +++ b/redisson/src/main/java/org/redisson/RedissonRx.java @@ -478,6 +478,16 @@ public class RedissonRx implements RedissonRxClient { return RxProxyBuilder.create(commandExecutor, new RedissonBitSet(commandExecutor, name), RBitSetRx.class); } + @Override + public RFunctionRx getFunction() { + return RxProxyBuilder.create(commandExecutor, new RedissonFuction(commandExecutor), RFunctionRx.class); + } + + @Override + public RFunctionRx getFunction(Codec codec) { + return RxProxyBuilder.create(commandExecutor, new RedissonFuction(commandExecutor, codec), RFunctionRx.class); + } + @Override public RScriptRx getScript() { return RxProxyBuilder.create(commandExecutor, new RedissonScript(commandExecutor), RScriptRx.class); diff --git a/redisson/src/main/java/org/redisson/api/FunctionLibrary.java b/redisson/src/main/java/org/redisson/api/FunctionLibrary.java new file mode 100644 index 000000000..2648d07ef --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/FunctionLibrary.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import java.util.List; + +/** + * Encapsulates information about Redis functions library. + * + * @author Nikita Koksharov + * + */ +public class FunctionLibrary { + + public enum Flag {NO_WRITES, ALLOW_OOM, ALLOW_STALE, NO_CLUSTER} + + public static class Function { + + private final String name; + private final String description; + private final List flags; + + public Function(String name, String description, List flags) { + this.name = name; + this.description = description; + this.flags = flags; + } + + public List getFlags() { + return flags; + } + + public String getDescription() { + return description; + } + + public String getName() { + return name; + } + } + + + private final String name; + private final String engine; + private final String description; + private final String code; + private final List functions; + + public FunctionLibrary(String name, String engine, String description, String code, List functions) { + this.name = name; + this.engine = engine; + this.description = description; + this.code = code; + this.functions = functions; + } + + public String getName() { + return name; + } + + public String getEngine() { + return engine; + } + + public String getDescription() { + return description; + } + + public String getCode() { + return code; + } + + public List getFunctions() { + return functions; + } +} diff --git a/redisson/src/main/java/org/redisson/api/FunctionStats.java b/redisson/src/main/java/org/redisson/api/FunctionStats.java new file mode 100644 index 000000000..dc3f3fbfe --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/FunctionStats.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * Encapsulates information about currently running + * Redis function and available execution engines. + * + * @author Nikita Koksharov + * + */ +public class FunctionStats { + + public static class Engine { + + private final Long libraries; + private final Long functions; + + public Engine(Long libraries, Long functions) { + this.libraries = libraries; + this.functions = functions; + } + + /** + * Returns libraries amount + * + * @return libraries amount + */ + public Long getLibraries() { + return libraries; + } + + /** + * Returns functions amount + * + * @return functions amount + */ + public Long getFunctions() { + return functions; + } + } + + public static class RunningFunction { + + private final String name; + private final List command; + private final Duration duration; + + public RunningFunction(String name, List command, Duration duration) { + this.name = name; + this.command = command; + this.duration = duration; + } + + /** + * Returns name of running function + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Returns arguments of running function + * + * @return arguments + */ + public List getCommand() { + return command; + } + + /** + * Returns runtime duration of running function + * + * @return runtime duration + */ + public Duration getDuration() { + return duration; + } + } + + private final RunningFunction runningFunction; + private final Map engines; + + public FunctionStats(RunningFunction runningFunction, Map engines) { + this.runningFunction = runningFunction; + this.engines = engines; + } + + /** + * Returns currently running fuction otherwise {@code null} + * + * @return running function + */ + public RunningFunction getRunningFunction() { + return runningFunction; + } + + /** + * Returns engine objects mapped by function engine name + * + * @return engine objects + */ + public Map getEngines() { + return engines; + } +} diff --git a/redisson/src/main/java/org/redisson/api/RFunction.java b/redisson/src/main/java/org/redisson/api/RFunction.java new file mode 100644 index 000000000..8dd459db8 --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/RFunction.java @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; + +import java.util.List; + +/** + * Interface for Redis Function feature + * + * @author Nikita Koksharov + * + */ +public interface RFunction extends RFunctionAsync { + + enum Mode { + /** + * Execute script as read operation + */ + READ, + + /** + * Execute function as read operation + */ + WRITE + } + + enum ReturnType { + /** + * Result is a value of Boolean type + */ + BOOLEAN(RedisCommands.FCALL_BOOLEAN_SAFE), + + /** + * Result is a value of Long type + */ + LONG(RedisCommands.FCALL_LONG), + + /** + * Result is a value of List type + */ + LIST(RedisCommands.FCALL_LIST), + + /** + * Result is a value of plain String type + */ + STRING(RedisCommands.FCALL_STRING), + + /** + * Result is a value of Object type + */ + VALUE(RedisCommands.FCALL_OBJECT), + + /** + * Result is a value of Map Value type. Codec.getMapValueDecoder() and Codec.getMapValueEncoder() + * methods are used for data deserialization or serialization. + */ + MAPVALUE(RedisCommands.FCALL_MAP_VALUE), + + /** + * Result is a value of List type, which consists of objects of Map Value type. + * Codec.getMapValueDecoder() and Codec.getMapValueEncoder() + * methods are used for data deserialization or serialization. + */ + MAPVALUELIST(RedisCommands.FCALL_MAP_VALUE_LIST); + + private final RedisCommand command; + + ReturnType(RedisCommand command) { + this.command = command; + } + + public RedisCommand getCommand() { + return command; + } + + }; + + /** + * Deletes library. Error is thrown if library doesn't exist. + * + * @param libraryName library name + */ + void delete(String libraryName); + + /** + * Returns serialized state of all libraries. + * + * @return serialized state + */ + byte[] dump(); + + /** + * Deletes all libraries. + * + */ + void flush(); + + /** + * Kills currently executed functions. + * Applied only to functions which don't modify data. + * + */ + void kill(); + + /** + * Returns information about libraries and functions per each. + * + * @return list of libraries + */ + List list(); + + /** + * Returns information about libraries and functions per each by name pattern. + *

+ * Supported glob-style patterns: + * h?llo matches hello, hallo and hxllo + * h*llo matches hllo and heeeello + * h[ae]llo matches hello and hallo, but not hillo + * + * @param namePattern name pattern + * @return list of libraries + */ + List list(String namePattern); + + /** + * Loads a library. Error is thrown if library already exists. + * + * @param libraryName library name + * @param code function code + */ + void load(String libraryName, String code); + + /** + * Loads a library and overwrites existing library. + * + * @param libraryName library name + * @param code function code + */ + void loadAndReplace(String libraryName, String code); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries and throws error in case of collision. + * + * @param payload serialized state + */ + void restore(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries. + * + * @param payload serialized state + */ + void restoreAndReplace(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Deletes all existing libraries before restoring. + * + * @param payload serialized state + */ + void restoreAfterFlush(byte[] payload); + + /** + * Returns information about currently running + * Redis function and available execution engines. + * + * @return function information + */ + FunctionStats stats(); + + /** + * Executes function + * + * @param - type of result + * @param key - used to locate Redis node in Cluster + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + R call(String key, Mode mode, String name, ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + R call(Mode mode, String name, ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @return result object + */ + R call(Mode mode, String name, ReturnType returnType); + +} diff --git a/redisson/src/main/java/org/redisson/api/RFunctionAsync.java b/redisson/src/main/java/org/redisson/api/RFunctionAsync.java new file mode 100644 index 000000000..1c18b2a9c --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/RFunctionAsync.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import org.redisson.api.RFunction.Mode; +import org.redisson.api.RFunction.ReturnType; + +import java.util.List; + +/** + * Interface for Redis Function feature + * + * @author Nikita Koksharov + * + */ +public interface RFunctionAsync { + + /** + * Deletes library. Error is thrown if library doesn't exist. + * + * @param libraryName library name + */ + RFuture deleteAsync(String libraryName); + + /** + * Returns serialized state of all libraries. + * + * @return serialized state + */ + RFuture dumpAsync(); + + /** + * Deletes all libraries. + * + */ + RFuture flushAsync(); + + /** + * Kills currently executed functions. + * Applied only to functions which don't modify data. + * + */ + RFuture killAsync(); + + /** + * Returns information about libraries and functions per each. + * + * @return list of libraries + */ + RFuture> listAsync(); + + /** + * Returns information about libraries and functions per each by name pattern. + *

+ * Supported glob-style patterns: + * h?llo matches hello, hallo and hxllo + * h*llo matches hllo and heeeello + * h[ae]llo matches hello and hallo, but not hillo + * + * @param namePattern name pattern + * @return list of libraries + */ + RFuture> listAsync(String namePattern); + + /** + * Loads a library. Error is thrown if library already exists. + * + * @param libraryName library name + * @param code function code + */ + RFuture loadAsync(String libraryName, String code); + + /** + * Loads a library and overwrites existing library. + * + * @param libraryName library name + * @param code function code + */ + RFuture loadAndReplaceAsync(String libraryName, String code); + + /** + * Restores libraries using their state returned by {@link #dumpAsync()} method. + * Restored libraries are appended to the existing libraries and throws error in case of collision. + * + * @param payload serialized state + */ + RFuture restoreAsync(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dumpAsync()} method. + * Restored libraries are appended to the existing libraries. + * + * @param payload serialized state + */ + RFuture restoreAndReplaceAsync(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dumpAsync()} method. + * Deletes all existing libraries before restoring. + * + * @param payload serialized state + */ + RFuture restoreAfterFlushAsync(byte[] payload); + + /** + * Returns information about currently running + * Redis function and available execution engines. + * + * @return function information + */ + RFuture statsAsync(); + + /** + * Executes function + * + * @param - type of result + * @param key - used to locate Redis node in Cluster which stores cached Lua script + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + RFuture callAsync(String key, Mode mode, String name, ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + RFuture callAsync(Mode mode, String name, RFunction.ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @return result object + */ + RFuture callAsync(Mode mode, String name, ReturnType returnType); + +} diff --git a/redisson/src/main/java/org/redisson/api/RFunctionReactive.java b/redisson/src/main/java/org/redisson/api/RFunctionReactive.java new file mode 100644 index 000000000..1810563c3 --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/RFunctionReactive.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Interface for Redis Function feature + * + * @author Nikita Koksharov + * + */ +public interface RFunctionReactive { + + /** + * Deletes library. Error is thrown if library doesn't exist. + * + * @param libraryName library name + */ + Mono delete(String libraryName); + + /** + * Returns serialized state of all libraries. + * + * @return serialized state + */ + Mono dump(); + + /** + * Deletes all libraries. + * + */ + Mono flush(); + + /** + * Kills currently executed functions. + * Applied only to functions which don't modify data. + * + */ + Mono kill(); + + /** + * Returns information about libraries and functions per each. + * + * @return list of libraries + */ + Mono> list(); + + /** + * Returns information about libraries and functions per each by name pattern. + *

+ * Supported glob-style patterns: + * h?llo matches hello, hallo and hxllo + * h*llo matches hllo and heeeello + * h[ae]llo matches hello and hallo, but not hillo + * + * @param namePattern name pattern + * @return list of libraries + */ + Mono> list(String namePattern); + + /** + * Loads a library. Error is thrown if library already exists. + * + * @param libraryName library name + * @param code function code + */ + Mono load(String libraryName, String code); + + /** + * Loads a library and overwrites existing library. + * + * @param libraryName library name + * @param code function code + */ + Mono loadAndReplace(String libraryName, String code); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries and throws error in case of collision. + * + * @param payload serialized state + */ + Mono restore(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries. + * + * @param payload serialized state + */ + Mono restoreAndReplace(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Deletes all existing libraries before restoring. + * + * @param payload serialized state + */ + Mono restoreAfterFlush(byte[] payload); + + /** + * Returns information about currently running + * Redis function and available execution engines. + * + * @return function information + */ + Mono stats(); + + /** + * Executes function + * + * @param - type of result + * @param key - used to locate Redis node in Cluster + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + Mono call(String key, RFunction.Mode mode, String name, RFunction.ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + Mono call(RFunction.Mode mode, String name, RFunction.ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @return result object + */ + Mono call(RFunction.Mode mode, String name, RFunction.ReturnType returnType); + +} diff --git a/redisson/src/main/java/org/redisson/api/RFunctionRx.java b/redisson/src/main/java/org/redisson/api/RFunctionRx.java new file mode 100644 index 000000000..1ed5c4493 --- /dev/null +++ b/redisson/src/main/java/org/redisson/api/RFunctionRx.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2013-2021 Nikita Koksharov + * + * 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 + * + * http://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 org.redisson.api; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; + +import java.util.List; + +/** + * Interface for Redis Function feature + * + * @author Nikita Koksharov + * + */ +public interface RFunctionRx { + + /** + * Deletes library. Error is thrown if library doesn't exist. + * + * @param libraryName library name + */ + Completable delete(String libraryName); + + /** + * Returns serialized state of all libraries. + * + * @return serialized state + */ + Single dump(); + + /** + * Deletes all libraries. + * + */ + Completable flush(); + + /** + * Kills currently executed functions. + * Applied only to functions which don't modify data. + * + */ + Completable kill(); + + /** + * Returns information about libraries and functions per each. + * + * @return list of libraries + */ + Single> list(); + + /** + * Returns information about libraries and functions per each by name pattern. + *

+ * Supported glob-style patterns: + * h?llo matches hello, hallo and hxllo + * h*llo matches hllo and heeeello + * h[ae]llo matches hello and hallo, but not hillo + * + * @param namePattern name pattern + * @return list of libraries + */ + Single> list(String namePattern); + + /** + * Loads a library. Error is thrown if library already exists. + * + * @param libraryName library name + * @param code function code + */ + Completable load(String libraryName, String code); + + /** + * Loads a library and overwrites existing library. + * + * @param libraryName library name + * @param code function code + */ + Completable loadAndReplace(String libraryName, String code); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries and throws error in case of collision. + * + * @param payload serialized state + */ + Completable restore(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Restored libraries are appended to the existing libraries. + * + * @param payload serialized state + */ + Completable restoreAndReplace(byte[] payload); + + /** + * Restores libraries using their state returned by {@link #dump()} method. + * Deletes all existing libraries before restoring. + * + * @param payload serialized state + */ + Completable restoreAfterFlush(byte[] payload); + + /** + * Returns information about currently running + * Redis function and available execution engines. + * + * @return function information + */ + Single stats(); + + /** + * Executes function + * + * @param - type of result + * @param key - used to locate Redis node in Cluster + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + Maybe call(String key, RFunction.Mode mode, String name, RFunction.ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @param keys - keys available through KEYS param in script + * @param values - values available through VALUES param in script + * @return result object + */ + Maybe call(RFunction.Mode mode, String name, RFunction.ReturnType returnType, List keys, Object... values); + + /** + * Executes function + * + * @param - type of result + * @param mode - execution mode + * @param name - function name + * @param returnType - return type + * @return result object + */ + Maybe call(RFunction.Mode mode, String name, RFunction.ReturnType returnType); + +} diff --git a/redisson/src/main/java/org/redisson/api/RedissonClient.java b/redisson/src/main/java/org/redisson/api/RedissonClient.java index de7901cff..b921cb8d9 100755 --- a/redisson/src/main/java/org/redisson/api/RedissonClient.java +++ b/redisson/src/main/java/org/redisson/api/RedissonClient.java @@ -1004,6 +1004,21 @@ public interface RedissonClient { */ RIdGenerator getIdGenerator(String name); + /** + * Returns interface for Redis Function feature + * + * @return function object + */ + RFunction getFunction(); + + /** + * Returns interface for Redis Function feature using provided codec + * + * @param codec - codec for params and result + * @return function interface + */ + RFunction getFunction(Codec codec); + /** * Returns script operations object * diff --git a/redisson/src/main/java/org/redisson/api/RedissonReactiveClient.java b/redisson/src/main/java/org/redisson/api/RedissonReactiveClient.java index 697f20bb1..edb104378 100644 --- a/redisson/src/main/java/org/redisson/api/RedissonReactiveClient.java +++ b/redisson/src/main/java/org/redisson/api/RedissonReactiveClient.java @@ -830,6 +830,21 @@ public interface RedissonReactiveClient { */ RBitSetReactive getBitSet(String name); + /** + * Returns interface for Redis Function feature + * + * @return function object + */ + RFunctionReactive getFunction(); + + /** + * Returns interface for Redis Function feature using provided codec + * + * @param codec - codec for params and result + * @return function interface + */ + RFunctionReactive getFunction(Codec codec); + /** * Returns script operations object * diff --git a/redisson/src/main/java/org/redisson/api/RedissonRxClient.java b/redisson/src/main/java/org/redisson/api/RedissonRxClient.java index bc88e5204..6fbb68e97 100644 --- a/redisson/src/main/java/org/redisson/api/RedissonRxClient.java +++ b/redisson/src/main/java/org/redisson/api/RedissonRxClient.java @@ -820,6 +820,21 @@ public interface RedissonRxClient { */ RBitSetRx getBitSet(String name); + /** + * Returns interface for Redis Function feature + * + * @return function object + */ + RFunctionRx getFunction(); + + /** + * Returns interface for Redis Function feature using provided codec + * + * @param codec - codec for params and result + * @return function interface + */ + RFunctionRx getFunction(Codec codec); + /** * Returns script operations object * diff --git a/redisson/src/main/java/org/redisson/client/protocol/RedisCommands.java b/redisson/src/main/java/org/redisson/client/protocol/RedisCommands.java index 122534641..413053a94 100644 --- a/redisson/src/main/java/org/redisson/client/protocol/RedisCommands.java +++ b/redisson/src/main/java/org/redisson/client/protocol/RedisCommands.java @@ -15,10 +15,7 @@ */ package org.redisson.client.protocol; -import org.redisson.api.FastAutoClaimResult; -import org.redisson.api.RType; -import org.redisson.api.StreamInfo; -import org.redisson.api.StreamMessageId; +import org.redisson.api.*; import org.redisson.client.codec.Codec; import org.redisson.client.codec.DoubleCodec; import org.redisson.client.codec.StringCodec; @@ -29,8 +26,10 @@ import org.redisson.client.protocol.pubsub.PubSubStatusDecoder; import org.redisson.cluster.ClusterNodeInfo; import org.redisson.misc.RedisURI; +import java.time.Duration; import java.util.*; import java.util.Map.Entry; +import java.util.stream.Collectors; /** * @@ -290,6 +289,84 @@ public interface RedisCommands { RedisCommand RPUSH_BOOLEAN = new RedisCommand("RPUSH", new TrueReplayConvertor()); RedisCommand RPUSH_VOID = new RedisCommand("RPUSH", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_DELETE = new RedisStrictCommand<>("FUNCTION", "DELETE", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_FLUSH = new RedisStrictCommand<>("FUNCTION", "FLUSH", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_KILL = new RedisStrictCommand<>("FUNCTION", "KILL", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_RESTORE = new RedisStrictCommand<>("FUNCTION", "RESTORE", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_LOAD = new RedisStrictCommand<>("FUNCTION", "LOAD", new VoidReplayConvertor()); + RedisStrictCommand FUNCTION_DUMP = new RedisStrictCommand<>("FUNCTION", "DUMP"); + RedisStrictCommand FUNCTION_STATS = new RedisStrictCommand<>("FUNCTION", "STATS", + new ListMultiDecoder2( + new CodecDecoder() { + @Override + public Object decode(List parts, State state) { + FunctionStats.RunningFunction runningFunction = (FunctionStats.RunningFunction) parts.get(1); + Map engines = (Map) parts.get(3); + return new FunctionStats(runningFunction, engines); + } + }, + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()) { + @Override + public Object decode(List parts, State state) { + if (parts.size() == 2) { + Map result = new HashMap<>(); + List objects = (List) parts.get(1); + Long libraries = (Long) objects.get(1); + Long functions = (Long) objects.get(3); + String engine = (String) parts.get(0); + result.put(engine, new FunctionStats.Engine(libraries, functions)); + return result; + } + String name = (String) parts.get(1); + List command = (List) parts.get(3); + Long duration = (Long) parts.get(5); + return new FunctionStats.RunningFunction(name, command, Duration.ofMillis(duration)); + } + }, + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()))); + + RedisStrictCommand FUNCTION_LIST = new RedisStrictCommand<>("FUNCTION", "LIST", + new ListMultiDecoder2( + new CodecDecoder(), + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()) { + @Override + public Object decode(List parts, State state) { + String name = (String) parts.get(1); + String engine = (String) parts.get(3); + String description = (String) parts.get(5); + String code = null; + if (parts.size() > 8) { + code = (String) parts.get(9); + } + List functions = (List) parts.get(7); + return new FunctionLibrary(name, engine, description, code, functions); + } + }, + new CodecDecoder(), + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()) { + @Override + public Object decode(List parts, State state) { + String functionName = (String) parts.get(1); + String functionDescription = (String) parts.get(3); + List functionFlags = ((List) parts.get(5)).stream() + .map(s -> FunctionLibrary.Flag.valueOf(s.toUpperCase().replace("-", "_"))) + .collect(Collectors.toList()); + return new FunctionLibrary.Function(functionName, functionDescription, functionFlags); + } + }, + new CodecDecoder())); + + RedisStrictCommand FCALL_BOOLEAN_SAFE = new RedisStrictCommand("FCALL", new BooleanNullSafeReplayConvertor()); + RedisStrictCommand FCALL_LONG = new RedisStrictCommand("FCALL"); + RedisCommand> FCALL_LIST = new RedisCommand>("FCALL", new ObjectListReplayDecoder()); + RedisStrictCommand FCALL_STRING = new RedisStrictCommand("FCALL", + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder())); + RedisCommand FCALL_OBJECT = new RedisCommand("FCALL"); + RedisCommand FCALL_MAP_VALUE = new RedisCommand("FCALL", new MapValueDecoder()); + RedisCommand> FCALL_MAP_VALUE_LIST = new RedisCommand>("FCALL", + new MapValueDecoder(new ObjectListReplayDecoder<>())); + + RedisStrictCommand SCRIPT_LOAD = new RedisStrictCommand("SCRIPT", "LOAD", new ObjectDecoder(new StringDataDecoder())); RedisStrictCommand SCRIPT_KILL = new RedisStrictCommand("SCRIPT", "KILL", new BooleanReplayConvertor()); RedisStrictCommand SCRIPT_FLUSH = new RedisStrictCommand("SCRIPT", "FLUSH", new BooleanReplayConvertor()); @@ -300,7 +377,7 @@ public interface RedisCommands { RedisStrictCommand EVAL_BOOLEAN_SAFE = new RedisStrictCommand("EVAL", new BooleanNullSafeReplayConvertor()); RedisStrictCommand EVAL_NULL_BOOLEAN = new RedisStrictCommand("EVAL", new BooleanNullReplayConvertor()); RedisStrictCommand EVAL_STRING = new RedisStrictCommand("EVAL", - new ObjectDecoder(new StringReplayDecoder())); + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder())); RedisStrictCommand EVAL_PERMIT_DATA = new RedisStrictCommand("EVAL", new ObjectDecoder(new PermitDecoder())); RedisStrictCommand EVAL_INTEGER = new RedisStrictCommand("EVAL", new IntegerReplayConvertor()); diff --git a/redisson/src/main/java/org/redisson/command/CommandAsyncExecutor.java b/redisson/src/main/java/org/redisson/command/CommandAsyncExecutor.java index f63273f67..c2d8fada1 100644 --- a/redisson/src/main/java/org/redisson/command/CommandAsyncExecutor.java +++ b/redisson/src/main/java/org/redisson/command/CommandAsyncExecutor.java @@ -73,6 +73,8 @@ public interface CommandAsyncExecutor { List> executeAll(RedisCommand command, Object... params); + List> executeMasters(RedisCommand command, Object... params); + RFuture writeAllAsync(RedisCommand command, SlotCallback callback, Object... params); RFuture> readAllAsync(Codec codec, RedisCommand command, Object... params); diff --git a/redisson/src/main/java/org/redisson/command/CommandAsyncService.java b/redisson/src/main/java/org/redisson/command/CommandAsyncService.java index a7595cbbf..f5eceb345 100644 --- a/redisson/src/main/java/org/redisson/command/CommandAsyncService.java +++ b/redisson/src/main/java/org/redisson/command/CommandAsyncService.java @@ -260,6 +260,16 @@ public class CommandAsyncService implements CommandAsyncExecutor { return writeAllAsync(command, null, params); } + @Override + public List> executeMasters(RedisCommand command, Object... params) { + List> futures = connectionManager.getEntrySet().stream().map(e -> { + RFuture f = async(false, new NodeSource(e), + connectionManager.getCodec(), command, params, true, false); + return f.toCompletableFuture(); + }).collect(Collectors.toList()); + return futures; + } + @Override public List> executeAll(RedisCommand command, Object... params) { Collection nodes = connectionManager.getEntrySet(); @@ -271,7 +281,7 @@ public class CommandAsyncService implements CommandAsyncExecutor { e.getAllEntries().stream().filter(c -> c.getNodeType() == NodeType.SLAVE).forEach(c -> { RFuture slavePromise = async(true, new NodeSource(e, c.getClient()), - connectionManager.getCodec(), RedisCommands.SCRIPT_LOAD, params, true, false); + connectionManager.getCodec(), command, params, true, false); futures.add(slavePromise.toCompletableFuture()); }); }); diff --git a/redisson/src/test/java/org/redisson/RedissonFunctionTest.java b/redisson/src/test/java/org/redisson/RedissonFunctionTest.java new file mode 100644 index 000000000..693f726c2 --- /dev/null +++ b/redisson/src/test/java/org/redisson/RedissonFunctionTest.java @@ -0,0 +1,99 @@ +package org.redisson; + +import org.junit.jupiter.api.Test; +import org.redisson.api.FunctionLibrary; +import org.redisson.api.FunctionStats; +import org.redisson.api.RFunction; +import org.redisson.client.codec.LongCodec; +import org.redisson.client.codec.StringCodec; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedissonFunctionTest extends BaseTest { + + @Test + public void testEmpty() { + RFunction f = redisson.getFunction(); + f.flush(); + assertThat(f.dump()).isNotEmpty(); + assertThat(f.list()).isEmpty(); + assertThat(f.list("test")).isEmpty(); + } + + @Test + public void testStats() { + RFunction f = redisson.getFunction(); + f.flush(); + f.load("lib", "redis.register_function('myfun', function(keys, args) for i = 1, 8829381983, 1 do end return args[1] end)" + + "redis.register_function('myfun2', function(keys, args) return 'test' end)" + + "redis.register_function('myfun3', function(keys, args) return 123 end)"); + f.callAsync(RFunction.Mode.READ, "myfun", RFunction.ReturnType.VALUE, Collections.emptyList(), "test"); + FunctionStats stats = f.stats(); + FunctionStats.RunningFunction func = stats.getRunningFunction(); + assertThat(func.getName()).isEqualTo("myfun"); + FunctionStats.Engine engine = stats.getEngines().get("LUA"); + assertThat(engine.getLibraries()).isEqualTo(1); + assertThat(engine.getFunctions()).isEqualTo(3); + + f.kill(); + FunctionStats stats2 = f.stats(); + assertThat(stats2.getRunningFunction()).isNull(); + } + + @Test + public void testCall() { + RFunction f = redisson.getFunction(); + f.flush(); + f.load("lib", "redis.register_function('myfun', function(keys, args) return args[1] end)" + + "redis.register_function('myfun2', function(keys, args) return 'test' end)" + + "redis.register_function('myfun3', function(keys, args) return 123 end)"); + String s = f.call(RFunction.Mode.READ, "myfun", RFunction.ReturnType.VALUE, Collections.emptyList(), "test"); + assertThat(s).isEqualTo("test"); + + RFunction f2 = redisson.getFunction(StringCodec.INSTANCE); + String s2 = f2.call(RFunction.Mode.READ, "myfun2", RFunction.ReturnType.STRING, Collections.emptyList()); + assertThat(s2).isEqualTo("test"); + + RFunction f3 = redisson.getFunction(LongCodec.INSTANCE); + Long s3 = f3.call(RFunction.Mode.READ, "myfun3", RFunction.ReturnType.LONG, Collections.emptyList()); + assertThat(s3).isEqualTo(123L); + } + + @Test + public void testList() { + RFunction f = redisson.getFunction(); + f.flush(); + f.load("lib", "redis.register_function('myfun', function(keys, args) return args[1] end)" + + "redis.register_function{function_name='myfun2', callback=function(keys, args) return args[1] end, flags={ 'no-writes' }}"); + + List data = f.list(); + FunctionLibrary fl = data.get(0); + assertThat(fl.getName()).isEqualTo("lib"); + FunctionLibrary.Function f2 = fl.getFunctions().stream().filter(e -> e.getName().equals("myfun2")).findFirst().get(); + assertThat(f2.getFlags()).containsExactly(FunctionLibrary.Flag.NO_WRITES); + } + + @Test + public void testListPattern() { + RFunction f = redisson.getFunction(); + f.flush(); + f.load("alib", "redis.register_function('myfun', function(keys, args) return args[1] end)"); + f.load("lib2", "redis.register_function('myfun2', function(keys, args) return args[1] end)"); + + List data = f.list("ali*"); + FunctionLibrary fl = data.get(0); + assertThat(data).hasSize(1); + assertThat(fl.getName()).isEqualTo("alib"); + + List data1 = f.list("ali2*"); + assertThat(data1).isEmpty(); + } + + + +} + +