Skip to content

Commit 0f28dad

Browse files
Mik13leibale
andauthored
Execute empty MULTI (#2423)
* Fix multi.exec with empty queue and previous watch When calling exec on a multi instance which you did not use, no command is sent currently. This is a problem for watched keys, because no EXEC means no unwatch, which might cause hard-to-debug problems. Proposed Fix: Sending UNWATCH * execute empty multi command (instead of skipping) * Update index.ts * Update index.ts * Update multi-command.ts * Update multi-command.ts * Update multi-command.ts * Update multi-command.ts * short circuit empty pipelines * Update index.ts --------- Co-authored-by: Leibale <me@leibale.com>
1 parent 1be8422 commit 0f28dad

File tree

6 files changed

+46
-45
lines changed

6 files changed

+46
-45
lines changed

packages/client/lib/client/index.spec.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -518,14 +518,23 @@ describe('Client', () => {
518518
);
519519
}, GLOBAL.SERVERS.OPEN);
520520

521-
testUtils.testWithClient('execAsPipeline', async client => {
522-
assert.deepEqual(
523-
await client.multi()
524-
.ping()
525-
.exec(true),
526-
['PONG']
527-
);
528-
}, GLOBAL.SERVERS.OPEN);
521+
describe('execAsPipeline', () => {
522+
testUtils.testWithClient('exec(true)', async client => {
523+
assert.deepEqual(
524+
await client.multi()
525+
.ping()
526+
.exec(true),
527+
['PONG']
528+
);
529+
}, GLOBAL.SERVERS.OPEN);
530+
531+
testUtils.testWithClient('empty execAsPipeline', async client => {
532+
assert.deepEqual(
533+
await client.multi().execAsPipeline(),
534+
[]
535+
);
536+
}, GLOBAL.SERVERS.OPEN);
537+
});
529538

530539
testUtils.testWithClient('should remember selected db', async client => {
531540
await client.multi()

packages/client/lib/client/index.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ export default class RedisClient<
460460
);
461461
} else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) {
462462
return Promise.reject(new ClientOfflineError());
463-
}
463+
}
464464

465465
const promise = this.#queue.addCommand<T>(args, options);
466466
this.#tick();
@@ -725,11 +725,14 @@ export default class RedisClient<
725725
return Promise.reject(new ClientClosedError());
726726
}
727727

728-
const promise = Promise.all(
729-
commands.map(({ args }) => {
730-
return this.#queue.addCommand(args, { chainId });
731-
})
732-
);
728+
const promise = chainId ?
729+
// if `chainId` has a value, it's a `MULTI` (and not "pipeline") - need to add the `MULTI` and `EXEC` commands
730+
Promise.all([
731+
this.#queue.addCommand(['MULTI'], { chainId }),
732+
this.#addMultiCommands(commands, chainId),
733+
this.#queue.addCommand(['EXEC'], { chainId })
734+
]) :
735+
this.#addMultiCommands(commands);
733736

734737
this.#tick();
735738

@@ -742,6 +745,12 @@ export default class RedisClient<
742745
return results;
743746
}
744747

748+
#addMultiCommands(commands: Array<RedisMultiQueuedCommand>, chainId?: symbol) {
749+
return Promise.all(
750+
commands.map(({ args }) => this.#queue.addCommand(args, { chainId }))
751+
);
752+
}
753+
745754
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
746755
let cursor = 0;
747756
do {

packages/client/lib/client/multi-command.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,9 @@ export default class RedisClientMultiCommand {
170170
return this.execAsPipeline();
171171
}
172172

173-
const commands = this.#multi.exec();
174-
if (!commands) return [];
175-
176173
return this.#multi.handleExecReplies(
177174
await this.#executor(
178-
commands,
175+
this.#multi.queue,
179176
this.#selectedDB,
180177
RedisMultiCommand.generateChainId()
181178
)
@@ -185,6 +182,8 @@ export default class RedisClientMultiCommand {
185182
EXEC = this.exec;
186183

187184
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
185+
if (this.#multi.queue.length === 0) return [];
186+
188187
return this.#multi.transformReplies(
189188
await this.#executor(
190189
this.#multi.queue,

packages/client/lib/cluster/multi-command.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,8 @@ export default class RedisClusterMultiCommand {
120120
return this.execAsPipeline();
121121
}
122122

123-
const commands = this.#multi.exec();
124-
if (!commands) return [];
125-
126123
return this.#multi.handleExecReplies(
127-
await this.#executor(commands, this.#firstKey, RedisMultiCommand.generateChainId())
124+
await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
128125
);
129126
}
130127

packages/client/lib/multi-command.spec.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,23 @@ describe('Multi Command', () => {
4646
});
4747

4848
describe('exec', () => {
49-
it('undefined', () => {
50-
assert.equal(
51-
new RedisMultiCommand().exec(),
52-
undefined
49+
it('without commands', () => {
50+
assert.deepEqual(
51+
new RedisMultiCommand().queue,
52+
[]
5353
);
5454
});
5555

56-
it('Array', () => {
56+
it('with commands', () => {
5757
const multi = new RedisMultiCommand();
5858
multi.addCommand(['PING']);
5959

6060
assert.deepEqual(
61-
multi.exec(),
62-
[
63-
{ args: ['MULTI'] },
64-
{ args: ['PING'], transformReply: undefined },
65-
{ args: ['EXEC'] }
66-
]
61+
multi.queue,
62+
[{
63+
args: ['PING'],
64+
transformReply: undefined
65+
}]
6766
);
6867
});
6968
});

packages/client/lib/multi-command.ts

-12
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,6 @@ export default class RedisMultiCommand {
6969
return transformedArguments;
7070
}
7171

72-
exec(): undefined | Array<RedisMultiQueuedCommand> {
73-
if (!this.queue.length) {
74-
return;
75-
}
76-
77-
return [
78-
{ args: ['MULTI'] },
79-
...this.queue,
80-
{ args: ['EXEC'] }
81-
];
82-
}
83-
8472
handleExecReplies(rawReplies: Array<RedisCommandRawReply>): Array<RedisCommandRawReply> {
8573
const execReply = rawReplies[rawReplies.length - 1] as (null | Array<RedisCommandRawReply>);
8674
if (execReply === null) {

0 commit comments

Comments
 (0)