Skip to content

Commit

Permalink
feat: support defining custom commands via constructor options
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Mar 12, 2022
1 parent ee463f1 commit 65082d6
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 18 deletions.
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ redis.zrange("sortedSet", 0, 2, "WITHSCORES").then((elements) => {
redis.set("mykey", "hello", "EX", 10);
```


See the `examples/` folder for more examples.

## Connect to Redis
Expand All @@ -164,7 +163,7 @@ new Redis({
password: "my-top-secret",
db: 0, // Defaults to 0
});
````
```

You can also specify connection options as a [`redis://` URL](http://www.iana.org/assignments/uri-schemes/prov/redis) or [`rediss://` URL](https://www.iana.org/assignments/uri-schemes/prov/rediss) when using [TLS encryption](#tls-options):

Expand Down Expand Up @@ -496,6 +495,8 @@ redis.myechoBuffer("k1", "k2", "a1", "a2", (err, result) => {
redis.pipeline().set("foo", "bar").myecho("k1", "k2", "a1", "a2").exec();
```

### Dynamic Keys

If the number of keys can't be determined when defining a command, you can
omit the `numberOfKeys` property and pass the number of keys as the first argument
when you call the command:
Expand All @@ -512,6 +513,25 @@ redis.echoDynamicKeyNumber(2, "k1", "k2", "a1", "a2", (err, result) => {
});
```

### As Constructor Options

Besides `defineCommand()`, you can also define custom commands with the `scripts` constructor option:

```javascript
const redis = new Redis({
scripts: {
myecho: {
numberOfKeys: 2,
lua: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}",
},
},
});
```

### TypeScript Usages

You can refer to [the example](examples/typescript/scripts.ts) for how to declare your custom commands.

## Transparent Key Prefixing

This feature allows you to specify a string that will automatically be prepended
Expand Down Expand Up @@ -769,7 +789,7 @@ const redis = new Redis({

Set maxRetriesPerRequest to `null` to disable this behavior, and every command will wait forever until the connection is alive again (which is the default behavior before ioredis v4).

### Reconnect on error
### Reconnect on Error

Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on certain Redis errors using the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error:

Expand Down Expand Up @@ -1020,7 +1040,7 @@ cluster.get("foo", (err, res) => {
- `slotsRefreshTimeout`: Milliseconds before a timeout occurs while refreshing slots from the cluster (default `1000`).
- `slotsRefreshInterval`: Milliseconds between every automatic slots refresh (default `5000`).
### Read-write splitting
### Read-Write Splitting
A typical redis cluster contains three or more masters and several slaves for each master. It's possible to scale out redis cluster by sending read queries to slaves and write queries to masters by setting the `scaleReads` option.

Expand Down Expand Up @@ -1049,7 +1069,7 @@ cluster.get("foo", (err, res) => {

**NB** In the code snippet above, the `res` may not be equal to "bar" because of the lag of replication between the master and slaves.

### Running commands to multiple nodes
### Running Commands to Multiple Nodes

Every command will be sent to exactly one node. For commands containing keys, (e.g. `GET`, `SET` and `HGETALL`), ioredis sends them to the node that serving the keys, and for other commands not containing keys, (e.g. `INFO`, `KEYS` and `FLUSHDB`), ioredis sends them to a random node.

Expand Down Expand Up @@ -1099,7 +1119,7 @@ const cluster = new Redis.Cluster(

This option is also useful when the cluster is running inside a Docker container.

### Transaction and pipeline in Cluster mode
### Transaction and Pipeline in Cluster Mode

Almost all features that are supported by `Redis` are also supported by `Redis.Cluster`, e.g. custom commands, transaction and pipeline.
However there are some differences when using transaction and pipeline in Cluster mode:
Expand Down Expand Up @@ -1178,7 +1198,7 @@ const cluster = new Redis.Cluster(
);
```

### Special note: AWS ElastiCache Clusters with TLS
### Special Note: Aws Elasticache Clusters with TLS

AWS ElastiCache for Redis (Clustered Mode) supports TLS encryption. If you use
this, you may encounter errors with invalid certificates. To resolve this
Expand Down Expand Up @@ -1223,7 +1243,7 @@ A pipeline will thus contain commands using different slots but that ultimately

Note that the same slot limitation within a single command still holds, as it is a Redis limitation.

### Example of automatic pipeline enqueuing
### Example of Automatic Pipeline Enqueuing

This sample code uses ioredis with automatic pipeline enabled.

Expand Down
36 changes: 36 additions & 0 deletions examples/typescript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions examples/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "typescript",
"version": "0.0.0",
"description": "",
"private": true,
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"typescript": "^4.6.2"
}
}
39 changes: 39 additions & 0 deletions examples/typescript/scripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Redis, { RedisCommander, Result, Callback } from "ioredis";
const redis = new Redis();

/**
* Define our command
*/
redis.defineCommand("myecho", {
numberOfKeys: 1,
lua: "return KEYS[1] .. ARGV[1]",
});

// Add declarations
declare module "ioredis" {
interface RedisCommander<Context> {
myecho(
key: string,
argv: string,
callback?: Callback<string>
): Result<string, Context>;
}
}

// Works with callbacks
redis.myecho("key", "argv", (err, result) => {
console.log("callback", result);
});

// Works with Promises
(async () => {
console.log("promise", await redis.myecho("key", "argv"));
})();

// Works with pipelining
redis
.pipeline()
.myecho("key", "argv")
.exec((err, result) => {
console.log("pipeline", result);
});
6 changes: 6 additions & 0 deletions lib/Redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ class Redis extends Commander {
this.connector = new StandaloneConnector(this.options);
}

if (this.options.scripts) {
Object.entries(this.options.scripts).forEach(([name, definition]) => {
this.defineCommand(name, definition);
});
}

// end(or wait) -> connecting -> connect -> ready -> end
if (this.options.lazyConnect) {
this.setStatus("wait");
Expand Down
8 changes: 8 additions & 0 deletions lib/cluster/ClusterOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ export interface ClusterOptions extends CommanderOptions {
* @default 60000
*/
maxScriptsCachingTime?: number;

/**
* Custom LUA commands
*/
scripts?: Record<
string,
{ lua: string; numberOfKeys?: number; readOnly?: boolean }
>;
}

export const DEFAULT_CLUSTER_OPTIONS: ClusterOptions = {
Expand Down
12 changes: 9 additions & 3 deletions lib/cluster/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { EventEmitter } from "events";
import { exists, hasFlag } from "@ioredis/commands";
import { EventEmitter } from "events";
import { AbortError, RedisError } from "redis-errors";
import asCallback from "standard-as-callback";
import Pipeline from "../Pipeline";
import Command from "../Command";
import ClusterAllFailedError from "../errors/ClusterAllFailedError";
import Pipeline from "../Pipeline";
import Redis from "../Redis";
import ScanStream from "../ScanStream";
import { addTransactionSupport, Transaction } from "../transaction";
import { Callback, ScanStreamOptions, WriteableStream } from "../types";
import {
CONNECTION_CLOSED_ERROR_MSG,
Expand Down Expand Up @@ -36,7 +37,6 @@ import {
weightSrvRecords,
} from "./util";
import Deque = require("denque");
import { addTransactionSupport, Transaction } from "../transaction";

const debug = Debug("cluster");

Expand Down Expand Up @@ -145,6 +145,12 @@ class Cluster extends Commander {

this.subscriber = new ClusterSubscriber(this.connectionPool, this);

if (this.options.scripts) {
Object.entries(this.options.scripts).forEach(([name, definition]) => {
this.defineCommand(name, definition);
});
}

if (this.options.lazyConnect) {
this.setStatus("wait");
} else {
Expand Down
14 changes: 14 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export { default as Cluster } from "./cluster";
*/
export { default as Command } from "./Command";

/**
* @ignore
*/
export {
default as RedisCommander,
Result,
ClientContext,
} from "./utils/RedisCommander";

/**
* @ignore
*/
Expand All @@ -31,6 +40,11 @@ export {
SentinelIterator,
} from "./connectors/SentinelConnector";

/**
* @ignore
*/
export { Callback } from "./types";

// Type Exports
export { SentinelAddress } from "./connectors/SentinelConnector";
export { RedisOptions } from "./redis/RedisOptions";
Expand Down
4 changes: 4 additions & 0 deletions lib/redis/RedisOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ interface CommonRedisOptions extends CommanderOptions {
enableOfflineQueue?: boolean;
enableReadyCheck?: boolean;
lazyConnect?: boolean;
scripts?: Record<
string,
{ lua: string; numberOfKeys?: number; readOnly?: boolean }
>;
}

export type RedisOptions = CommonRedisOptions &
Expand Down
42 changes: 42 additions & 0 deletions test/functional/cluster/scripting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import MockServer from "../../helpers/mock_server";
import { expect } from "chai";
import { Cluster } from "../../../lib";

describe("cluster:scripting", () => {
it("should throw when not all keys in a pipeline command belong to the same slot", async () => {
const lua = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}";
const handler = (argv) => {
if (argv[0] === "cluster" && argv[1] === "SLOTS") {
return [
[0, 12181, ["127.0.0.1", 30001]],
[12182, 16383, ["127.0.0.1", 30002]],
];
}
console.log(argv);
if (argv[0] === "eval" && argv[1] === lua && argv[2] === "2") {
return argv.slice(3);
}
};
new MockServer(30001, handler);
new MockServer(30002, handler);

const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], {
scripts: { test: { lua, numberOfKeys: 2 }, testDynamic: { lua } },
});

// @ts-expect-error
expect(await cluster.test("{foo}1", "{foo}2", "argv1", "argv2")).to.eql([
"{foo}1",
"{foo}2",
"argv1",
"argv2",
]);

expect(
// @ts-expect-error
await cluster.testDynamic(2, "{foo}1", "{foo}2", "argv1", "argv2")
).to.eql(["{foo}1", "{foo}2", "argv1", "argv2"]);

cluster.disconnect();
});
});
Loading

0 comments on commit 65082d6

Please sign in to comment.