Skip to content

Commit

Permalink
Extend EXECABORT with "previous errors" redis#4084
Browse files Browse the repository at this point in the history
Propagate errors in transaction on command queuing (e.g before actual EXEC).
Errors are added as suppressed exceptions when EXEC is processed.

https://redis.io/docs/latest/develop/interact/transactions/
-A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, ...), or there may be some critical condition like an out of memory condition (if the server is configured to have a memory limit using the maxmemory directive).

 closes redis#4084
  • Loading branch information
ggivo committed Feb 11, 2025
1 parent 8e5cf66 commit 6d20b3a
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 2 deletions.
18 changes: 16 additions & 2 deletions src/main/java/redis/clients/jedis/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,25 @@ public List<Object> exec() {
try {
// ignore QUEUED (or ERROR)
// processPipelinedResponses(pipelinedResponses.size());
connection.getMany(1 + pipelinedResponses.size());
List<Object> queuedCmdResponses = connection.getMany(1 + pipelinedResponses.size());


connection.sendCommand(EXEC);

List<Object> unformatted = connection.getObjectMultiBulkReply();
List<Object> unformatted;
try {
unformatted = connection.getObjectMultiBulkReply();
} catch (JedisDataException jce) {
// A command may fail to be queued, so there may be an error before EXEC is called
// In this case, the server will discard all commands in the transaction and return the EXECABORT error.
// Enhance the final error with suppressed errors.
queuedCmdResponses.stream()
.filter(o -> o instanceof Exception)
.map(o -> (Exception) o)
.forEach(jce::addSuppressed);
throw jce;
}

if (unformatted == null) {
pipelinedResponses.clear();
return null;
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/redis/clients/jedis/TransactionV2Test.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package redis.clients.jedis;

import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue;
import static redis.clients.jedis.Protocol.Command.INCR;
import static redis.clients.jedis.Protocol.Command.GET;
import static redis.clients.jedis.Protocol.Command.SET;
Expand All @@ -9,6 +11,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.hamcrest.MatcherAssert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -213,6 +217,23 @@ public void transactionResponseWithError() {
assertEquals("bar", r.get());
}

@Test
public void transactionPropagatesErrorsBeforeExec() {
// A command may fail to be queued, so there may be an error before EXEC is called.
// For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, ...)
CommandObject<String> invalidCommand =
new CommandObject<>(new CommandObjects().commandArguments(PipeliningTest.Foo.FOO), BuilderFactory.STRING);

Transaction t = new Transaction(conn);
t.appendCommand(invalidCommand);
t.set("foo","bar");
JedisDataException exception = assertThrows(JedisDataException.class, t::exec);
Throwable[] suppressed = exception.getSuppressed();
assertNotNull("Suppressed exceptions should not be null", suppressed);
assertTrue("There should be at least one suppressed exception", suppressed.length > 0);
MatcherAssert.assertThat(suppressed[0].getMessage(), containsString("ERR unknown command 'FOO'"));
}

@Test
public void testCloseable() {
Transaction transaction = new Transaction(conn);
Expand Down

0 comments on commit 6d20b3a

Please sign in to comment.