Skip to content

Commit dea46c5

Browse files
uglideggivo
andauthored
Add commands flags based on static map (#4332)
* Add commands flags based on static map * Clean up CommandFlagsRegistryGenerator * more fixes to CommandFlagsRegistryGenerator * Introduce CommandFlagsRegistry interface - Add new interface and move CommandFlag enum there - Generate StaticCommandFlagsRegistry instead of embedding generated code into CommandObject - Allow passing custom CommandFlagsRegistry to ClusterClientBuilder * Fix formatting * Add support for sub-commands * Use CommandArguments in the interface and hierarchical approach in static registry * Expose get() on CommandArguments for cleaner code * Use byte array based look up to improve performance * (clean up) Separate generated command flags registry initialisation from actual implementation (#4345) * (clean up) Extract generated registry initialization * regenerate after clean up * reformat * Clean up StaticCommandFlagsRegistry --------- Co-authored-by: Ivo Gaydazhiev <ivo.gaydazhiev@redis.com>
1 parent 4776709 commit dea46c5

15 files changed

+1952
-20
lines changed

pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@
360360
<!-- Exclude integration tests from unit-test phase -->
361361
<exclude>**/*IntegrationTest.java</exclude>
362362
<exclude>**/*IntegrationTests.java</exclude>
363+
<!-- Exclude code generators - these are tools, not tests -->
364+
<exclude>**/codegen/**/*.java</exclude>
363365
</excludes>
364366
<!--<trimStackTrace>false</trimStackTrace>-->
365367
</configuration>
@@ -535,6 +537,7 @@
535537
<include>**/MultiDb*.java</include>
536538
<include>**/ClientTestUtil.java</include>
537539
<include>**/ReflectionTestUtil.java</include>
540+
<include>**/*CommandFlags*.java</include>
538541
</includes>
539542
</configuration>
540543
<executions>

src/main/java/redis/clients/jedis/CommandArguments.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ public int size() {
195195
return args.size();
196196
}
197197

198+
/**
199+
* Get the argument at the specified index.
200+
* @param index the index of the argument to retrieve (0-based, where 0 is the command itself)
201+
* @return the Rawable argument at the specified index
202+
* @throws IndexOutOfBoundsException if the index is out of range
203+
*/
204+
public Rawable get(int index) {
205+
return args.get(index);
206+
}
207+
198208
@Override
199209
public Iterator<Rawable> iterator() {
200210
return args.iterator();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package redis.clients.jedis;
2+
3+
import java.util.EnumSet;
4+
5+
/**
6+
* Registry interface for command flags. Provides a mapping from Redis commands to their flags. This
7+
* interface allows for different implementations of the flags registry.
8+
*/
9+
public interface CommandFlagsRegistry {
10+
11+
/**
12+
* Command flags based on command flags exposed by Redis. See
13+
* <a href="https://redis.io/docs/latest/commands/command/#flags">Command flags</a> for more
14+
* details.
15+
* <p>
16+
* Flags description:
17+
* <ul>
18+
* <li>READONLY: Command doesn't modify data</li>
19+
* <li>WRITE: Command may modify data</li>
20+
* <li>DENYOOM: Command may increase memory usage (deny if out of memory)</li>
21+
* <li>ADMIN: Administrative command</li>
22+
* <li>PUBSUB: Pub/Sub related command</li>
23+
* <li>NOSCRIPT: Command not allowed in scripts</li>
24+
* <li>SORT_FOR_SCRIPT: Command output needs sorting for scripts</li>
25+
* <li>LOADING: Command allowed while database is loading</li>
26+
* <li>STALE: Command allowed on stale replicas</li>
27+
* <li>SKIP_MONITOR: Command not shown in MONITOR output</li>
28+
* <li>ASKING: Command allowed in cluster ASKING state</li>
29+
* <li>FAST: Command has O(1) time complexity</li>
30+
* <li>MOVABLEKEYS: Command key positions may vary</li>
31+
* <li>MODULE: Module command</li>
32+
* <li>BLOCKING: Command may block the client</li>
33+
* <li>NO_AUTH: Command allowed without authentication</li>
34+
* <li>NO_ASYNC_LOADING: Command not allowed during async loading</li>
35+
* <li>NO_MULTI: Command not allowed in MULTI/EXEC</li>
36+
* <li>NO_MANDATORY_KEYS: Command may work without keys</li>
37+
* <li>ALLOW_BUSY: Command allowed when server is busy</li>
38+
* </ul>
39+
*/
40+
enum CommandFlag {
41+
READONLY, WRITE, DENYOOM, ADMIN, PUBSUB, NOSCRIPT, SORT_FOR_SCRIPT, LOADING, STALE,
42+
SKIP_MONITOR, SKIP_SLOWLOG, ASKING, FAST, MOVABLEKEYS, MODULE, BLOCKING, NO_AUTH,
43+
NO_ASYNC_LOADING, NO_MULTI, NO_MANDATORY_KEYS, ALLOW_BUSY
44+
}
45+
46+
/**
47+
* Get the flags for a given command.
48+
* @param commandArguments the command arguments containing the command and its parameters
49+
* @return EnumSet of CommandFlag for this command, or empty set if command has no flags
50+
*/
51+
EnumSet<CommandFlag> getFlags(CommandArguments commandArguments);
52+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package redis.clients.jedis;
2+
3+
import redis.clients.jedis.args.Rawable;
4+
import redis.clients.jedis.commands.ProtocolCommand;
5+
import redis.clients.jedis.util.JedisByteMap;
6+
import redis.clients.jedis.util.SafeEncoder;
7+
8+
import java.util.EnumSet;
9+
import java.util.Map;
10+
11+
/**
12+
* Static implementation of CommandFlagsRegistry.
13+
*/
14+
public class StaticCommandFlagsRegistry implements CommandFlagsRegistry {
15+
16+
// Empty flags constant for commands with no flags
17+
public static final EnumSet<CommandFlag> EMPTY_FLAGS = EnumSet.noneOf(CommandFlag.class);
18+
19+
// Singleton instance
20+
private static final StaticCommandFlagsRegistry REGISTRY = createRegistry();
21+
22+
private final Commands commands;
23+
24+
private StaticCommandFlagsRegistry(Commands commands) {
25+
this.commands = commands;
26+
}
27+
28+
/**
29+
* Get the singleton instance of the static command flags registry.
30+
* <p>
31+
* DO NOT USE THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING.
32+
* </p>
33+
* @return StaticCommandFlagsRegistry
34+
*/
35+
public static StaticCommandFlagsRegistry registry() {
36+
return REGISTRY;
37+
}
38+
39+
private static StaticCommandFlagsRegistry createRegistry() {
40+
41+
Builder builder = new Builder();
42+
43+
// Delegate population to generated class
44+
StaticCommandFlagsRegistryInitializer.initialize(builder);
45+
46+
return builder.build();
47+
}
48+
49+
/**
50+
* Get the flags for a given command. Flags are looked up from a static registry based on the
51+
* command arguments. This approach significantly reduces memory usage by sharing flag instances
52+
* across all CommandObject instances.
53+
* <p>
54+
* For commands with subcommands (e.g., FUNCTION LOAD, ACL SETUSER), this method implements a
55+
* hierarchical lookup strategy:
56+
* <ol>
57+
* <li>First, retrieve the parent command using CommandArguments.getCommand()</li>
58+
* <li>Check if this is a parent command (has subcommands in the registry)</li>
59+
* <li>If it is a parent command, attempt to get the child/subcommand by:
60+
* <ul>
61+
* <li>Extracting the second argument from the CommandArguments object</li>
62+
* <li>Matching this second argument against the child items of the parent command</li>
63+
* </ul>
64+
* </li>
65+
* <li>Return the appropriate flags based on whether a child command was found or just use the
66+
* parent command's flags</li>
67+
* </ol>
68+
* @param commandArguments the command arguments containing the command and its parameters
69+
* @return EnumSet of CommandFlag for this command, or empty set if command has no flags
70+
*/
71+
@Override
72+
public EnumSet<CommandFlag> getFlags(CommandArguments commandArguments) {
73+
// Get the parent command
74+
ProtocolCommand cmd = commandArguments.getCommand();
75+
byte[] raw = cmd.getRaw();
76+
77+
// Convert to uppercase using SafeEncoder utility (faster than String.toUpperCase())
78+
byte[] uppercaseBytes = SafeEncoder.toUpperCase(raw);
79+
80+
// Look up the parent command in the registry using byte array key
81+
// Object registryEntry = COMMAND_FLAGS_REGISTRY.get(uppercaseBytes);
82+
CommandMeta commandMeta = commands.getCommand(uppercaseBytes);
83+
84+
if (commandMeta == null) {
85+
// Command not found in registry
86+
return EMPTY_FLAGS;
87+
}
88+
89+
if (!commandMeta.hasSubcommands()) {
90+
// Check if this is a simple command without subcommands
91+
return commandMeta.getFlags();
92+
} else {
93+
// Parent command with subcommands
94+
// Try to extract the subcommand from the second argument
95+
byte[] subCommand = getSubCommand(commandArguments);
96+
if (subCommand != null) {
97+
CommandMeta subCommandMeta = commandMeta.getSubcommand(subCommand);
98+
if (subCommandMeta != null) {
99+
return subCommandMeta.getFlags();
100+
} else {
101+
// (second argument exists but not a recognized subcommand , return parent flags
102+
return commandMeta.getFlags();
103+
}
104+
} else {
105+
// no second argument (no subcommand), return parent flags
106+
return commandMeta.getFlags();
107+
}
108+
}
109+
}
110+
111+
private byte[] getSubCommand(CommandArguments commandArguments) {
112+
if (commandArguments.size() > 1) {
113+
Rawable secondArg = commandArguments.get(1);
114+
byte[] subRaw = secondArg.getRaw();
115+
116+
// Convert to uppercase using SafeEncoder utility
117+
return SafeEncoder.toUpperCase(subRaw);
118+
} else {
119+
return null;
120+
}
121+
}
122+
123+
// Internal class to hold subcommand mappings for parent commands.
124+
static class Commands {
125+
126+
final JedisByteMap<CommandMeta> commands = new JedisByteMap<>();
127+
128+
boolean isEmpty() {
129+
return commands.isEmpty();
130+
}
131+
132+
public Commands register(byte[] cmd, CommandMeta command) {
133+
commands.put(cmd, command);
134+
return this;
135+
}
136+
137+
public boolean containsKey(byte[] command) {
138+
return commands.containsKey(command);
139+
}
140+
141+
public CommandMeta getCommand(byte[] command) {
142+
return commands.get(command);
143+
}
144+
145+
public Map<byte[], CommandMeta> getCommands() {
146+
return commands;
147+
}
148+
}
149+
150+
//
151+
static class CommandMeta {
152+
153+
final EnumSet<CommandFlag> flags;
154+
155+
final Commands subcommands = new Commands();
156+
157+
CommandMeta(EnumSet<CommandFlag> flags) {
158+
this.flags = flags;
159+
}
160+
161+
void putSubCommand(byte[] subCommand, CommandMeta subCommandMeta) {
162+
this.subcommands.register(subCommand, subCommandMeta);
163+
}
164+
165+
boolean hasSubcommands() {
166+
return !subcommands.isEmpty();
167+
}
168+
169+
EnumSet<CommandFlag> getFlags() {
170+
if (flags == null) {
171+
return EMPTY_FLAGS;
172+
}
173+
174+
return flags;
175+
}
176+
177+
CommandMeta getSubcommand(byte[] subcommand) {
178+
return subcommands.getCommand(subcommand);
179+
}
180+
}
181+
182+
static public class Builder {
183+
184+
private final Commands commands = new Commands();
185+
186+
public Builder register(String name, EnumSet<CommandFlag> flags) {
187+
commands.register(SafeEncoder.encode(name), new CommandMeta(flags));
188+
return this;
189+
}
190+
191+
public Builder register(String name, String subcommand, EnumSet<CommandFlag> flags) {
192+
byte[] cmdName = SafeEncoder.encode(name);
193+
194+
if (!commands.containsKey(cmdName)) {
195+
commands.register(SafeEncoder.encode(name), new CommandMeta(EMPTY_FLAGS));
196+
}
197+
198+
byte[] subCmdName = SafeEncoder.encode(subcommand);
199+
commands.getCommand(cmdName).putSubCommand(subCmdName, new CommandMeta(flags));
200+
return this;
201+
}
202+
203+
public StaticCommandFlagsRegistry build() {
204+
return new StaticCommandFlagsRegistry(commands);
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)