Skip to content

Commit c930d70

Browse files
zeniosa-TODO-rov
andauthored
RedisPublisher must not drop an error when the state changes concurrently #3942 (#3943)
* RedisPublisher must not drop an error when the state changes concurrently #3942 State.onError moved the subscription to COMPLETED with a single CAS in the loop condition. When the subscribing thread changed the state at the same moment (DEMAND -> READING -> DEMAND while reading), the CAS failed, the loop exited and the error was never signalled, so the subscriber waited forever. onError now retries the CAS until the subscription is completed. * Use explicit Mockito imports in RedisPublisherUnitTests #3942 --------- Co-authored-by: Aleksandar Todorov <a_t_todorov@yahoo.com>
1 parent 3576c0a commit c930d70

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎src/main/java/io/lettuce/core/RedisPublisher.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,15 +713,21 @@ void onAllDataRead(RedisSubscription<?> subscription) {
713713
void onError(RedisSubscription<?> subscription, Throwable t) {
714714

715715
State state;
716-
while ((state = subscription.state()) != COMPLETED && subscription.changeState(state, COMPLETED)) {
716+
while ((state = subscription.state()) != COMPLETED) {
717+
718+
// The subscribing thread may move the state (e.g. DEMAND -> READING -> DEMAND) between the read above and
719+
// the CAS. Retry instead of dropping the error, otherwise the subscriber is never terminated.
720+
if (!subscription.changeState(state, COMPLETED)) {
721+
continue;
722+
}
717723

718724
readData(subscription);
719725

720726
Subscriber<?> subscriber = subscription.subscriber;
721727
if (subscriber != null) {
722728
subscriber.onError(t);
723-
return;
724729
}
730+
return;
725731
}
726732
}
727733

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.lettuce.core;
2+
3+
import static io.lettuce.TestTags.UNIT_TEST;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.verifyNoMoreInteractions;
8+
9+
import org.junit.jupiter.api.Tag;
10+
import org.junit.jupiter.api.Test;
11+
import org.reactivestreams.Subscriber;
12+
13+
import io.lettuce.core.api.StatefulConnection;
14+
import io.lettuce.core.codec.StringCodec;
15+
import io.lettuce.core.output.StatusOutput;
16+
import io.lettuce.core.protocol.Command;
17+
import io.lettuce.core.protocol.CommandType;
18+
import io.netty.util.concurrent.ImmediateEventExecutor;
19+
20+
/**
21+
* Unit tests for {@link RedisPublisher}.
22+
*/
23+
@Tag(UNIT_TEST)
24+
class RedisPublisherUnitTests {
25+
26+
@Test
27+
void shouldSignalErrorWhenStateChangesConcurrently() {
28+
29+
Command<String, String, String> command = new Command<>(CommandType.EVALSHA, new StatusOutput<>(StringCodec.UTF8),
30+
null);
31+
32+
// Simulates the subscribing thread flipping DEMAND -> READING right before the error's CAS to COMPLETED.
33+
RedisPublisher.RedisSubscription<String> subscription = new RedisPublisher.RedisSubscription<String>(
34+
mock(StatefulConnection.class), command, false, ImmediateEventExecutor.INSTANCE) {
35+
36+
private boolean raced;
37+
38+
@Override
39+
boolean changeState(RedisPublisher.State oldState, RedisPublisher.State newState) {
40+
41+
if (!raced && oldState == RedisPublisher.State.DEMAND && newState == RedisPublisher.State.COMPLETED) {
42+
raced = true;
43+
super.changeState(RedisPublisher.State.DEMAND, RedisPublisher.State.READING);
44+
}
45+
return super.changeState(oldState, newState);
46+
}
47+
48+
};
49+
50+
Subscriber<String> subscriber = mock(Subscriber.class);
51+
subscription.subscribe(subscriber);
52+
subscription.changeState(RedisPublisher.State.NO_DEMAND, RedisPublisher.State.DEMAND);
53+
54+
RedisCommandExecutionException error = new RedisCommandExecutionException("NOSCRIPT No matching script");
55+
subscription.onError(error);
56+
57+
verify(subscriber).onSubscribe(any());
58+
verify(subscriber).onError(error);
59+
verifyNoMoreInteractions(subscriber);
60+
}
61+
62+
}

0 commit comments

Comments
 (0)