Skip to content

Commit 19d158b

Browse files
committed
import POC
1 parent 62ac8b7 commit 19d158b

File tree

12 files changed

+650
-4
lines changed

12 files changed

+650
-4
lines changed

examples/lua-multi-incr.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const client = createClient({
77
scripts: {
88
mincr: defineScript({
99
NUMBER_OF_KEYS: 2,
10+
// TODO add RequestPolicy: ,
1011
SCRIPT:
1112
'return {' +
1213
'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' +

packages/client/lib/cluster/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ClientSideCacheConfig, PooledClientSideCacheProvider } from '../client/
1313
import { BasicCommandParser } from '../client/parser';
1414
import { ASKING_CMD } from '../commands/ASKING';
1515
import SingleEntryCache from '../single-entry-cache'
16+
import { POLICIES, PolicyResolver, StaticPolicyResolver } from './request-response-policies';
1617
interface ClusterCommander<
1718
M extends RedisModules,
1819
F extends RedisFunctions,
@@ -187,6 +188,7 @@ export default class RedisCluster<
187188
return async function (this: ProxyCluster, ...args: Array<unknown>) {
188189
const parser = new BasicCommandParser();
189190
command.parseCommand(parser, ...args);
191+
console.log(parser, parser.redisArgs[0]);
190192

191193
return this._self._execute(
192194
parser.firstKey,
@@ -299,6 +301,7 @@ export default class RedisCluster<
299301

300302
private _self = this;
301303
private _commandOptions?: ClusterCommandOptions<TYPE_MAPPING/*, POLICIES*/>;
304+
private _policyResolver: PolicyResolver;
302305

303306
/**
304307
* An array of the cluster slots, each slot contain its `master` and `replicas`.
@@ -356,6 +359,8 @@ export default class RedisCluster<
356359
if (options?.commandOptions) {
357360
this._commandOptions = options.commandOptions;
358361
}
362+
363+
this._policyResolver = new StaticPolicyResolver(POLICIES);
359364
}
360365

361366
duplicate<
@@ -456,7 +461,11 @@ export default class RedisCluster<
456461
options: ClusterCommandOptions | undefined,
457462
fn: (client: RedisClientType<M, F, S, RESP, TYPE_MAPPING>, opts?: ClusterCommandOptions) => Promise<T>
458463
): Promise<T> {
464+
console.log(`executing command `, firstKey, isReadonly, options);
459465
const maxCommandRedirections = this._options.maxCommandRedirections ?? 16;
466+
const p = this._policyResolver.resolvePolicy("ping")
467+
console.log(`ping policy `, p);
468+
460469
let client = await this._slots.getClient(firstKey, isReadonly);
461470
let i = 0;
462471

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// import { RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from "../../RESP/types";
2+
// import { ShardNode } from "../cluster-slots";
3+
// import type { Either } from './types';
4+
5+
// export interface CommandRouter<
6+
// M extends RedisModules,
7+
// F extends RedisFunctions,
8+
// S extends RedisScripts,
9+
// RESP extends RespVersions,
10+
// TYPE_MAPPING extends TypeMapping> {
11+
// routeCommand(
12+
// command: string,
13+
// policy: RequestPolicy,
14+
// ): Either<ShardNode<M, F, S, RESP, TYPE_MAPPING>, 'no-available-nodes' | 'routing-failed'>;
15+
// }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { CommandReply } from '../../commands/generic-transformers';
2+
import type { CommandPolicies } from './policies-constants';
3+
import { REQUEST_POLICIES_WITH_DEFAULTS, RESPONSE_POLICIES_WITH_DEFAULTS } from './policies-constants';
4+
import type { PolicyResolver } from './types';
5+
import { StaticPolicyResolver } from './static-policy-resolver';
6+
import type { ModulePolicyRecords } from './static-policies-data';
7+
8+
/**
9+
* Function type that returns command information from Redis
10+
*/
11+
export type CommandFetcher = () => Promise<Array<CommandReply>>;
12+
13+
/**
14+
* A factory for creating policy resolvers that dynamically build policies based on the Redis server's COMMAND response.
15+
*
16+
* This factory fetches command information from Redis and analyzes the response to determine
17+
* appropriate routing policies for each command, returning a StaticPolicyResolver with the built policies.
18+
*/
19+
export class DynamicPolicyResolverFactory {
20+
/**
21+
* Creates a StaticPolicyResolver by fetching command information from Redis
22+
* and building appropriate policies based on the command characteristics.
23+
*
24+
* @param commandFetcher Function to fetch command information from Redis
25+
* @param fallbackResolver Optional fallback resolver to use when policies are not found
26+
* @returns A new StaticPolicyResolver with the fetched policies
27+
*/
28+
static async create(
29+
commandFetcher: CommandFetcher,
30+
fallbackResolver?: PolicyResolver
31+
): Promise<StaticPolicyResolver> {
32+
const commands = await commandFetcher();
33+
const policies: ModulePolicyRecords = {};
34+
35+
for (const command of commands) {
36+
const parsed = DynamicPolicyResolverFactory.#parseCommandName(command.name);
37+
38+
// Skip commands with invalid format (more than one dot)
39+
if (!parsed) {
40+
continue;
41+
}
42+
43+
const { moduleName, commandName } = parsed;
44+
45+
// Initialize module if it doesn't exist
46+
if (!policies[moduleName]) {
47+
policies[moduleName] = {};
48+
}
49+
50+
// Determine policies for this command
51+
const commandPolicies = DynamicPolicyResolverFactory.#buildCommandPolicies(command);
52+
policies[moduleName][commandName] = commandPolicies;
53+
}
54+
55+
return new StaticPolicyResolver(policies, fallbackResolver);
56+
}
57+
58+
/**
59+
* Parses a command name to extract module and command components.
60+
*
61+
* Redis commands can be in format:
62+
* - "ping" -> module: "std", command: "ping"
63+
* - "ft.search" -> module: "ft", command: "search"
64+
*
65+
* Commands with more than one dot are invalid.
66+
*/
67+
static #parseCommandName(fullCommandName: string): { moduleName: string; commandName: string } | null {
68+
const parts = fullCommandName.split('.');
69+
70+
if (parts.length === 1) {
71+
return { moduleName: 'std', commandName: fullCommandName };
72+
}
73+
74+
if (parts.length === 2) {
75+
return { moduleName: parts[0], commandName: parts[1] };
76+
}
77+
78+
// Commands with more than one dot are invalid in Redis
79+
return null;
80+
}
81+
82+
/**
83+
* Builds CommandPolicies for a command based on its characteristics.
84+
*
85+
* Priority order:
86+
* 1. Use explicit policies from the command if available
87+
* 2. Classify as DEFAULT_KEYLESS if keySpecification is empty
88+
* 3. Classify as DEFAULT_KEYED if keySpecification is not empty
89+
*/
90+
static #buildCommandPolicies(command: CommandReply): CommandPolicies {
91+
// Determine if command is keyless based on keySpecification
92+
const isKeyless = command.keySpecifications === 'keyless';
93+
94+
// Determine default policies based on key specification
95+
const defaultRequest = isKeyless
96+
? REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS
97+
: REQUEST_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED;
98+
const defaultResponse = isKeyless
99+
? RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYLESS
100+
: RESPONSE_POLICIES_WITH_DEFAULTS.DEFAULT_KEYED;
101+
102+
return {
103+
// request: command.policies.request ?? defaultRequest,
104+
// response: command.policies.response ?? defaultResponse
105+
request: defaultRequest,
106+
response: defaultResponse
107+
};
108+
}
109+
}

0 commit comments

Comments
 (0)