Skip to content

Commit

Permalink
feat: support username in URI
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Feb 5, 2021
1 parent c7d80a7 commit 7122509
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 26 deletions.
49 changes: 29 additions & 20 deletions README.md
Expand Up @@ -134,6 +134,13 @@ You can also specify connection options as a [`redis://` URL](http://www.iana.or
```javascript
// Connect to 127.0.0.1:6380, db 4, using password "authpassword":
new Redis("redis://:authpassword@127.0.0.1:6380/4");

// Username can also be passed via URI.
// It's worth to noticing that for compatibility reasons `allowUsernameInURI`
// need to be provided, otherwise the username part will be ignored.
new Redis(
"redis://username:authpassword@127.0.0.1:6380/4?allowUsernameInURI=true"
);
```

See [API Documentation](API.md#new_Redis) for all available options.
Expand Down Expand Up @@ -680,7 +687,6 @@ This feature is useful when using Amazon ElastiCache instances with Auto-failove

On ElastiCache insances with Auto-failover enabled, `reconnectOnError` does not execute. Instead of returning a Redis error, AWS closes all connections to the master endpoint until the new primary node is ready. ioredis reconnects via `retryStrategy` instead of `reconnectOnError` after about a minute. On ElastiCache insances with Auto-failover enabled, test failover events with the `Failover primary` option in the AWS console.


## Connection Events

The Redis instance will emit some events about the state of the connection to the Redis server.
Expand Down Expand Up @@ -923,13 +929,17 @@ Sometimes you may want to send a command to multiple nodes (masters or slaves) o
```javascript
// Send `FLUSHDB` command to all slaves:
const slaves = cluster.nodes("slave");
Promise.all(slaves.map(node => node.flushdb()))
Promise.all(slaves.map((node) => node.flushdb()));

// Get keys of all the masters:
const masters = cluster.nodes("master");
Promise.all(masters.map(node => node.keys()).then(keys => {
// keys: [['key1', 'key2'], ['key3', 'key4']]
}));
Promise.all(
masters
.map((node) => node.keys())
.then((keys) => {
// keys: [['key1', 'key2'], ['key3', 'key4']]
})
);
```

### NAT Mapping
Expand Down Expand Up @@ -1064,7 +1074,7 @@ const cluster = new Redis.Cluster(

## Autopipelining

In standard mode, when you issue multiple commands, ioredis sends them to the server one by one. As described in Redis pipeline documentation, this is a suboptimal use of the network link, especially when such link is not very performant.
In standard mode, when you issue multiple commands, ioredis sends them to the server one by one. As described in Redis pipeline documentation, this is a suboptimal use of the network link, especially when such link is not very performant.

The TCP and network overhead negatively affects performance. Commands are stuck in the send queue until the previous ones are correctly delivered to the server. This is a problem known as Head-Of-Line blocking (HOL).

Expand All @@ -1076,38 +1086,39 @@ This feature can dramatically improve throughput and avoids HOL blocking. In our

While an automatic pipeline is executing, all new commands will be enqueued in a new pipeline which will be executed as soon as the previous finishes.

When using Redis Cluster, one pipeline per node is created. Commands are assigned to pipelines according to which node serves the slot.
When using Redis Cluster, one pipeline per node is created. Commands are assigned to pipelines according to which node serves the slot.

A pipeline will thus contain commands using different slots but that ultimately are assigned to the same node.
A pipeline will thus contain commands using different slots but that ultimately are assigned to the same node.

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


### Example of automatic pipeline enqueuing

This sample code uses ioredis with automatic pipeline enabled.

```javascript
const Redis = require('./built');
const http = require('http');
const Redis = require("./built");
const http = require("http");

const db = new Redis({ enableAutoPipelining: true });

const server = http.createServer((request, response) => {
const key = new URL(request.url, 'https://localhost:3000/').searchParams.get('key');
const key = new URL(request.url, "https://localhost:3000/").searchParams.get(
"key"
);

db.get(key, (err, value) => {
response.writeHead(200, { 'Content-Type': 'text/plain' });
response.writeHead(200, { "Content-Type": "text/plain" });
response.end(value);
});
})
});

server.listen(3000);
```

When Node receives requests, it schedules them to be processed in one or more iterations of the events loop.

All commands issued by requests processing during one iteration of the loop will be wrapped in a pipeline automatically created by ioredis.
All commands issued by requests processing during one iteration of the loop will be wrapped in a pipeline automatically created by ioredis.

In the example above, the pipeline will have the following contents:

Expand All @@ -1129,24 +1140,22 @@ This approach increases the utilization of the network link, reduces the TCP ove

### Benchmarks

Here's some of the results of our tests for a single node.
Here's some of the results of our tests for a single node.

Each iteration of the test runs 1000 random commands on the server.

| | Samples | Result | Tolerance |
|---------------------------|---------|---------------|-----------|
| ------------------------- | ------- | ------------- | --------- |
| default | 1000 | 174.62 op/sec | 卤 0.45 % |
| enableAutoPipelining=true | 1500 | 233.33 op/sec | 卤 0.88 % |


And here's the same test for a cluster of 3 masters and 3 replicas:

| | Samples | Result | Tolerance |
|---------------------------|---------|---------------|-----------|
| ------------------------- | ------- | ------------- | --------- |
| default | 1000 | 164.05 op/sec | 卤 0.42 % |
| enableAutoPipelining=true | 3000 | 235.31 op/sec | 卤 0.94 % |


# Error Handling

All the errors returned by the Redis server are instances of `ReplyError`, which can be accessed via `Redis`:
Expand Down
15 changes: 12 additions & 3 deletions lib/utils/index.ts
Expand Up @@ -255,10 +255,19 @@ export function parseURL(url) {
parsed = urllibParse(url, true, true);
}

const options = parsed.query || {};
const allowUsernameInURI =
options.allowUsernameInURI && options.allowUsernameInURI !== "false";
delete options.allowUsernameInURI;

const result: any = {};
if (parsed.auth) {
const index = parsed.auth.indexOf(":")
result.password = index === -1 ? '' : parsed.auth.slice(index + 1)
const index = parsed.auth.indexOf(":");
if (allowUsernameInURI) {
result.username =
index === -1 ? parsed.auth : parsed.auth.slice(0, index);
}
result.password = index === -1 ? "" : parsed.auth.slice(index + 1);
}
if (parsed.pathname) {
if (parsed.protocol === "redis:" || parsed.protocol === "rediss:") {
Expand All @@ -275,7 +284,7 @@ export function parseURL(url) {
if (parsed.port) {
result.port = parsed.port;
}
defaults(result, parsed.query);
defaults(result, options);

return result;
}
Expand Down
19 changes: 19 additions & 0 deletions test/functional/auth.ts
Expand Up @@ -170,6 +170,25 @@ describe("auth", function () {
redis = new Redis({ port: 17379, username, password });
});

it("should handle auth with Redis URL string with username and password (Redis >=6) (redis://foo:bar@baz.com/) correctly", function (done) {
let username = "user";
let password = "pass";
let redis;
new MockServer(17379, function (argv) {
if (
argv[0] === "auth" &&
argv[1] === username &&
argv[2] === password
) {
redis.disconnect();
done();
}
});
redis = new Redis(
`redis://user:pass@localhost:17379/?allowUsernameInURI=true`
);
});

it('should not emit "error" when the Redis >=6 server doesn\'t need auth', function (done) {
new MockServer(17379, function (argv) {
if (argv[0] === "auth" && argv[1] === "pass") {
Expand Down
57 changes: 54 additions & 3 deletions test/unit/utils.ts
Expand Up @@ -204,9 +204,7 @@ describe("utils", function () {
password: "pass:word",
key: "value",
});
expect(
utils.parseURL("redis://user@127.0.0.1:6380/4?key=value")
).to.eql({
expect(utils.parseURL("redis://user@127.0.0.1:6380/4?key=value")).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
Expand All @@ -226,6 +224,59 @@ describe("utils", function () {
key: "value",
});
});

it("supports allowUsernameInURI", function () {
expect(
utils.parseURL(
"redis://user:pass@127.0.0.1:6380/4?allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "pass",
});
expect(
utils.parseURL(
"redis://user:pass@127.0.0.1:6380/4?allowUsernameInURI=false"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
password: "pass",
});
expect(
utils.parseURL(
"redis://user:pass:word@127.0.0.1:6380/4?key=value&allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "pass:word",
key: "value",
});
expect(
utils.parseURL(
"redis://user@127.0.0.1:6380/4?key=value&allowUsernameInURI=true"
)
).to.eql({
host: "127.0.0.1",
port: "6380",
db: "4",
username: "user",
password: "",
key: "value",
});
expect(
utils.parseURL("redis://127.0.0.1/?allowUsernameInURI=true")
).to.eql({
host: "127.0.0.1",
});
});
});

describe(".sample", function () {
Expand Down

0 comments on commit 7122509

Please sign in to comment.