Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(db): select Redis DB index on single-instance non-cluster configurations #10

Merged
merged 4 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ Redlock is designed to use [ioredis](https://github.com/luin/ioredis) to keep it
A redlock object is instantiated with an array of at least one redis client and an optional `options` object. Properties of the Redlock object should NOT be changed after it is first used, as doing so could have unintended consequences for live locks.

```ts
import Client from "ioredis";
import Redis from "ioredis";
import { Redlock } from "@sesamecare-oss/redlock";

const redisA = new Client({ host: "a.redis.example.com" });
const redisB = new Client({ host: "b.redis.example.com" });
const redisC = new Client({ host: "c.redis.example.com" });
const redisA = new Redis({ host: "a.redis.example.com" });
const redisB = new Redis({ host: "b.redis.example.com" });
const redisC = new Redis({ host: "c.redis.example.com" });

const redlock = new Redlock(
// You should have one client for each independent redis node
Expand Down Expand Up @@ -146,6 +146,31 @@ Please view the (very concise) source code or TypeScript definitions for a detai

Please see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for information on developing, running, and testing this library.

### Using a specific DB index

If you're using only one `Redis` client, with only one redis instance which has cluster mode **disabled**, you can set a `db` property in the `options` configuration to specify the DB index in which to store the lock records. For example:

```ts
import Redis from "ioredis";
import { Redlock } from "@sesamecare-oss/redlock";

const redis = new Redis({ host: "a.redis.example.com" });

const redlock = new Redlock(
[redis],
{
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
retryCount: 10,
retryDelay: 200, // time in ms
retryJitter: 200, // time in ms
automaticExtensionThreshold: 500, // time in ms
db: 2 // DB to select and use for lock records
}
);
```

Note that the `db` value is ignored for redis servers with cluster mode enabled. If a value outside of the range 0-15 is provided, the configuration defaults back to `0`.

### High-Availability Recommendations

- Use at least 3 independent servers or clusters
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"clean": "yarn dlx rimraf ./dist",
"lint": "eslint .",
"postinstall": "coconfig",
"test": "vitest"
"test": "vitest",
"test:unit": "vitest unit.spec.ts",
"test:system": "vitest system.spec.ts"
},
"keywords": [
"typescript",
Expand Down
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface Settings {
readonly retryDelay: number;
readonly retryJitter: number;
readonly automaticExtensionThreshold: number;
readonly db: number;
}

// Define default settings.
Expand All @@ -59,6 +60,7 @@ const defaultSettings: Readonly<Settings> = {
retryDelay: 200,
retryJitter: 100,
automaticExtensionThreshold: 500,
db: 0
};

// Modifyng this object is forbidden.
Expand Down Expand Up @@ -168,6 +170,11 @@ export class Redlock extends EventEmitter {
typeof settings.automaticExtensionThreshold === 'number'
? settings.automaticExtensionThreshold
: defaultSettings.automaticExtensionThreshold,
db:
(typeof settings.db === 'number' && Number.isInteger(settings.db) && settings.db >= 0 && settings.db <= 15)
// settings.db value must be a number and between 0 and 15, inclusive.
? settings.db
: defaultSettings.db,
};
}

Expand Down Expand Up @@ -216,7 +223,7 @@ export class Redlock extends EventEmitter {
const { attempts, start } = await this._execute(
'acquireLock',
resources,
[value, duration],
[this.settings.db, value, duration],
settings,
);

Expand All @@ -228,7 +235,7 @@ export class Redlock extends EventEmitter {
} catch (error) {
// If there was an error acquiring the lock, release any partial lock
// state that may exist on a minority of clients.
await this._execute('releaseLock', resources, [value], {
await this._execute('releaseLock', resources, [this.settings.db, value], {
retryCount: 0,
}).catch(() => {
// Any error here will be ignored.
Expand All @@ -250,7 +257,7 @@ export class Redlock extends EventEmitter {
lock.expiration = 0;

// Attempt to release the lock.
return this._execute('releaseLock', lock.resources, [lock.value], settings);
return this._execute('releaseLock', lock.resources, [this.settings.db, lock.value], settings);
}

/**
Expand All @@ -273,7 +280,7 @@ export class Redlock extends EventEmitter {
const { attempts, start } = await this._execute(
'extendLock',
existing.resources,
[existing.value, duration],
[this.settings.db, existing.value, duration],
settings,
);

Expand Down
19 changes: 15 additions & 4 deletions src/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { Redis as IORedisClient, Cluster as IORedisCluster, Result } from 'iored
type Client = IORedisClient | IORedisCluster;

// Define script constants.
const DB_SELECT_SCRIPT = `
-- Protected call to execute SELECT command if supported
redis.pcall("SELECT", tonumber(ARGV[1]))
`;

const ACQUIRE_SCRIPT = `
${DB_SELECT_SCRIPT}

-- Return 0 if an entry already exists.
for i, key in ipairs(KEYS) do
if redis.call("exists", key) == 1 then
Expand All @@ -12,35 +19,39 @@ const ACQUIRE_SCRIPT = `

-- Create an entry for each provided key.
for i, key in ipairs(KEYS) do
redis.call("set", key, ARGV[1], "PX", ARGV[2])
redis.call("set", key, ARGV[2], "PX", ARGV[3])
end

-- Return the number of entries added.
return #KEYS
`;

const EXTEND_SCRIPT = `
${DB_SELECT_SCRIPT}

-- Return 0 if an entry exists with a *different* lock value.
for i, key in ipairs(KEYS) do
if redis.call("get", key) ~= ARGV[1] then
if redis.call("get", key) ~= ARGV[2] then
return 0
end
end

-- Update the entry for each provided key.
for i, key in ipairs(KEYS) do
redis.call("set", key, ARGV[1], "PX", ARGV[2])
redis.call("set", key, ARGV[2], "PX", ARGV[3])
end

-- Return the number of entries updated.
return #KEYS
`;

const RELEASE_SCRIPT = `
${DB_SELECT_SCRIPT}

local count = 0
for i, key in ipairs(KEYS) do
-- Only remove entries for *this* lock value.
if redis.call("get", key) == ARGV[1] then
if redis.call("get", key) == ARGV[2] then
redis.pcall("del", key)
count = count + 1
end
Expand Down
49 changes: 27 additions & 22 deletions src/index.spec.ts → src/system.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,27 @@ function is<T>(actual: T, expected: T, message?: string): void {
}

describe.each([
{ type: 'instance' },
{ type: 'cluster' },
])('$type', ({ type }) => {
{ type: 'instance' }, // Defaults to db 0
{ type: 'instance', db: 2 }, // Test using db 2
{ type: 'cluster' }, // DB select not supported in cluster mode
])('$type', ({ type, db = 0 }) => {
const redis =
type === 'instance'
? new Redis({ host: 'redis-single-instance' })
: new Cluster([{ host: 'redis-single-cluster-1' }]);

beforeEach(async () => {
await (redis instanceof Cluster && redis.isCluster ? waitForCluster(redis) : null);
if (redis instanceof Cluster && redis.isCluster) {
await waitForCluster(redis);
} else {
await redis.select(db);
}
await redis.keys('*').then((keys) => (keys?.length ? redis.del(keys) : null));
});

test(`${type} - refuses to use a non-integer duration`, async () => {
test(`${type} - db ${db} - refuses to use a non-integer duration`, async () => {
try {
const redlock = new Redlock([redis]);
const redlock = new Redlock([redis], { db });
const duration = Number.MAX_SAFE_INTEGER / 10;

// Acquire a lock.
Expand All @@ -64,8 +69,8 @@ describe.each([
}
});

test(`${type} - acquires, extends, and releases a single lock`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - acquires, extends, and releases a single lock`, async () => {
const redlock = new Redlock([redis], { db });

const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10);

Expand All @@ -90,8 +95,8 @@ describe.each([
expect(await redis.get('{redlock}a')).toBeNull();
});

test(`${type} - acquires, extends, and releases a multi-resource lock`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - acquires, extends, and releases a multi-resource lock`, async () => {
const redlock = new Redlock([redis], { db });

const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10);

Expand Down Expand Up @@ -131,7 +136,7 @@ describe.each([
is(await redis.get('{redlock}a2'), null);
});

test(`${type} - locks fail when redis is unreachable`, async () => {
test(`${type} - db ${db} - locks fail when redis is unreachable`, async () => {
const redis = new Redis({
host: '127.0.0.1',
port: 6380,
Expand All @@ -146,7 +151,7 @@ describe.each([
// ignore redis-generated errors
});

const redlock = new Redlock([redis]);
const redlock = new Redlock([redis], { db });

const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10);
try {
Expand Down Expand Up @@ -174,8 +179,8 @@ describe.each([
}
});

test(`${type} - locks automatically expire`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - locks automatically expire`, async () => {
const redlock = new Redlock([redis], { db });

const duration = 200;

Expand All @@ -195,8 +200,8 @@ describe.each([
is(await redis.get('{redlock}d'), null);
});

test(`${type} - individual locks are exclusive`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - individual locks are exclusive`, async () => {
const redlock = new Redlock([redis], { db });

const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10);

Expand Down Expand Up @@ -241,8 +246,8 @@ describe.each([
is(await redis.get('{redlock}c'), null);
});

test(`${type} - overlapping multi-locks are exclusive`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - overlapping multi-locks are exclusive`, async () => {
const redlock = new Redlock([redis], { db });

const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10);

Expand Down Expand Up @@ -307,8 +312,8 @@ describe.each([
is(await redis.get('{redlock}c3'), null);
});

test(`${type} - the \`using\` helper acquires, extends, and releases locks`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - the \`using\` helper acquires, extends, and releases locks`, async () => {
const redlock = new Redlock([redis], { db });

const duration = 500;

Expand Down Expand Up @@ -339,8 +344,8 @@ describe.each([
is(await redis.get('{redlock}x'), null, 'The lock was not released.');
});

test(`${type} - the \`using\` helper is exclusive`, async () => {
const redlock = new Redlock([redis]);
test(`${type} - db ${db} - the \`using\` helper is exclusive`, async () => {
const redlock = new Redlock([redis], { db });

const duration = 500;

Expand Down
Loading
Loading