Skip to content

Commit 5db7830

Browse files
authored
RATIS-2521. LifeCycle.startAndTransition(..) may cause illegal transition: CLOSING -> EXCEPTION. (#1452)
1 parent d05c1b4 commit 5db7830

2 files changed

Lines changed: 75 additions & 5 deletions

File tree

ratis-common/src/main/java/org/apache/ratis/util/LifeCycle.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,18 @@ public String toString() {
273273
/** Run the given start method and transition the current state accordingly. */
274274
@SafeVarargs
275275
public final <T extends Throwable> void startAndTransition(
276-
CheckedRunnable<T> startImpl, Class<? extends Throwable>... exceptionClasses)
276+
CheckedRunnable<T> startMethod, Class<? extends Throwable>... exceptionClasses)
277277
throws T {
278278
transition(State.STARTING);
279279
try {
280-
startImpl.run();
280+
startMethod.run();
281281
transition(State.RUNNING);
282282
} catch (Throwable t) {
283-
transition(ReflectionUtils.isInstance(t, exceptionClasses)?
284-
State.NEW: State.EXCEPTION);
283+
final State state = getCurrentState();
284+
LOG.warn("{}: Failed to start (state={})", name, state, t);
285+
if (!state.isClosingOrClosed()) {
286+
transition(ReflectionUtils.isInstance(t, exceptionClasses) ? State.NEW : State.EXCEPTION);
287+
}
285288
throw t;
286289
}
287290
}

ratis-test/src/test/java/org/apache/ratis/util/TestLifeCycle.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Licensed to the Apache Software Foundation (ASF) under one
33
* or more contributor license agreements. See the NOTICE file
44
* distributed with this work for additional information
@@ -25,10 +25,13 @@
2525
import java.util.EnumMap;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.concurrent.CompletableFuture;
29+
import java.util.concurrent.ExecutionException;
2830

2931
import static org.apache.ratis.util.LifeCycle.State.*;
3032
import static org.junit.jupiter.api.Assertions.assertEquals;
3133
import static org.junit.jupiter.api.Assertions.assertFalse;
34+
import static org.junit.jupiter.api.Assertions.assertSame;
3235
import static org.junit.jupiter.api.Assertions.assertTrue;
3336

3437

@@ -101,4 +104,68 @@ private static void testInvalidTransition(TriConsumer<LifeCycle.State, LifeCycle
101104
}
102105
}
103106

107+
@Test
108+
public void testStartAndTransition() throws Exception {
109+
final SimulatedServer simulatedServer = new SimulatedServer();
110+
assertEquals(NEW, simulatedServer.getLifeCycleState());
111+
112+
final CompletableFuture<Throwable> f = CompletableFuture.supplyAsync(() -> {
113+
try {
114+
simulatedServer.start();
115+
throw new AssertionError("start() should fail");
116+
} catch (Exception e) {
117+
return e.getCause();
118+
}
119+
});
120+
121+
Thread.sleep(100);
122+
assertEquals(STARTING, simulatedServer.getLifeCycleState());
123+
124+
// call close() during STARTING, start() should throw the simulated exception
125+
CompletableFuture.supplyAsync(simulatedServer::close);
126+
assertSame(simulatedServer.getSimulatedException(), f.get());
127+
128+
assertEquals(CLOSING, simulatedServer.getLifeCycleState());
129+
simulatedServer.getCloseFuture().complete(null);
130+
Thread.sleep(100);
131+
assertEquals(CLOSED, simulatedServer.getLifeCycleState());
132+
}
133+
134+
private static final class SimulatedServer {
135+
private final LifeCycle lifeCycle = new LifeCycle(getClass().getSimpleName());
136+
private final Exception simulatedException = new Exception("Simulated exception");
137+
private final CompletableFuture<Void> startFuture = new CompletableFuture<>();
138+
private final CompletableFuture<Void> closeFuture = new CompletableFuture<>();
139+
140+
LifeCycle.State getLifeCycleState() {
141+
return lifeCycle.getCurrentState();
142+
}
143+
144+
Exception getSimulatedException() {
145+
return simulatedException;
146+
}
147+
148+
CompletableFuture<Void> getCloseFuture() {
149+
return closeFuture;
150+
}
151+
152+
void start() throws Exception {
153+
lifeCycle.startAndTransition(this::startImpl);
154+
}
155+
156+
void startImpl() throws Exception {
157+
startFuture.get();
158+
}
159+
160+
Void close() {
161+
// simulate close and then cause start() to fail.
162+
lifeCycle.checkStateAndClose(this::closeImpl);
163+
return null;
164+
}
165+
166+
void closeImpl() {
167+
startFuture.completeExceptionally(simulatedException);
168+
closeFuture.join();
169+
}
170+
}
104171
}

0 commit comments

Comments
 (0)