Skip to content

Commit fd8b29a

Browse files
authored
Update PotentialDeadlockException to account for scheduling delay (#2964)
* Add tests to reproduce thread starvation * Update docs for deadlock detection timeout
1 parent 4c0e493 commit fd8b29a

6 files changed

Lines changed: 62 additions & 19 deletions

File tree

temporal-sdk/src/main/java/io/temporal/internal/sync/DeterministicRunner.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ static DeterministicRunner newRunner(
5252
* completed or blocked.
5353
*
5454
* @throws Throwable if one of the threads didn't handle an exception.
55-
* @param deadlockDetectionTimeout the maximum time in milliseconds a thread can run without
56-
* calling yield.
55+
* @param deadlockDetectionTimeout the maximum time in milliseconds after a thread is scheduled
56+
* until it yields or completes.
5757
*/
5858
void runUntilAllBlocked(long deadlockDetectionTimeout);
5959

temporal-sdk/src/main/java/io/temporal/internal/sync/PotentialDeadlockException.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ public class PotentialDeadlockException extends RuntimeException {
4343
this.detectionTimestamp = detectionTimestamp;
4444
}
4545

46+
/**
47+
* @param workflowThreadContext context of the thread that is in a potential deadlock state
48+
* @param detectionTimestamp a timestamp the deadlock was detected
49+
* @param timeoutMillis configured deadlock detection timeout in milliseconds
50+
*/
51+
PotentialDeadlockException(
52+
WorkflowThreadContext workflowThreadContext, long detectionTimestamp, long timeoutMillis) {
53+
super(
54+
"[TMPRL1101] Potential deadlock detected. Workflow thread could not start executing within "
55+
+ formatTimeout(timeoutMillis)
56+
+ ".");
57+
this.workflowThreadContext = workflowThreadContext;
58+
this.detectionTimestamp = detectionTimestamp;
59+
}
60+
4661
private static String formatTimeout(long timeoutMillis) {
4762
if (timeoutMillis % 1000 == 0) {
4863
return timeoutMillis / 1000 + "s";

temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowThread.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ static WorkflowThread newThread(Runnable runnable, boolean detached, String name
6565
SyncWorkflowContext getWorkflowContext();
6666

6767
/**
68-
* @param deadlockDetectionTimeoutMs maximum time in milliseconds the thread can run before
69-
* calling yield.
68+
* @param deadlockDetectionTimeoutMs the maximum time in milliseconds after a thread is scheduled
69+
* until it yields or completes.
7070
* @return true if coroutine made some progress.
7171
*/
7272
boolean runUntilBlocked(long deadlockDetectionTimeoutMs);

temporal-sdk/src/main/java/io/temporal/internal/sync/WorkflowThreadContext.java

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,8 @@
1010
import java.util.concurrent.locks.Lock;
1111
import java.util.function.Supplier;
1212
import javax.annotation.Nullable;
13-
import org.slf4j.Logger;
14-
import org.slf4j.LoggerFactory;
1513

1614
class WorkflowThreadContext {
17-
private static final Logger log = LoggerFactory.getLogger(WorkflowThreadContext.class);
18-
1915
// Shared runner lock
2016
private final Lock runnerLock;
2117
private final WorkflowThreadScheduler scheduler;
@@ -214,8 +210,8 @@ public String getYieldReason() {
214210
}
215211

216212
/**
217-
* @param deadlockDetectionTimeoutMs maximum time in milliseconds the thread can run before
218-
* calling yield. Discarded if {@code TEMPORAL_DEBUG} env variable is set.
213+
* @param deadlockDetectionTimeoutMs the maximum time in milliseconds after a thread is scheduled
214+
* until it yields or completes. Discarded if {@code TEMPORAL_DEBUG} env variable is set.
219215
* @return true if thread made some progress. Which is await was unblocked and some code after it
220216
* * was executed.
221217
*/
@@ -241,13 +237,10 @@ public boolean runUntilBlocked(long deadlockDetectionTimeoutMs) {
241237
throw new PotentialDeadlockException(
242238
currentThread.getName(), this, detectionTimestamp, deadlockDetectionTimeoutMs);
243239
} else {
244-
// This should never happen.
245-
// We clear currentThread only after setting the status to DONE.
246-
// And we check for it by the status condition check after waking up on the condition
247-
// and acquiring the lock back
248-
log.warn("Illegal State: WorkflowThreadContext has no currentThread in {} state", status);
240+
// We clear currentThread only after setting the status to DONE, so this case should
241+
// only happen if the WorkflowThread is starving.
249242
throw new PotentialDeadlockException(
250-
"UnknownThread", this, detectionTimestamp, deadlockDetectionTimeoutMs);
243+
this, detectionTimestamp, deadlockDetectionTimeoutMs);
251244
}
252245
}
253246
Preconditions.checkState(

temporal-sdk/src/main/java/io/temporal/worker/WorkerOptions.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,9 @@ public Builder setLocalActivityWorkerOnly(boolean localActivityWorkerOnly) {
327327
/**
328328
* @param defaultDeadlockDetectionTimeoutMs time period in ms that will be used to detect
329329
* workflows deadlock. Default is 1000ms, which is chosen if set to zero.
330-
* <p>Specifies an amount of time in milliseconds that workflow tasks are allowed to execute
331-
* without interruption. If workflow task runs longer than specified interval without
332-
* yielding (like calling an Activity), it will fail automatically.
330+
* <p>Specifies a time interval in milliseconds within which a workflow task must yield
331+
* (like calling an Activity) or complete. If a workflow task runs longer than the specified
332+
* interval or takes too long to begin running, it will fail automatically.
333333
* @return {@code this}
334334
* @see io.temporal.internal.sync.PotentialDeadlockException
335335
*/

temporal-sdk/src/test/java/io/temporal/internal/sync/DeterministicRunnerTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
import java.util.Map;
3535
import java.util.Objects;
3636
import java.util.Optional;
37+
import java.util.concurrent.CountDownLatch;
3738
import java.util.concurrent.ExecutorService;
39+
import java.util.concurrent.Executors;
3840
import java.util.concurrent.SynchronousQueue;
3941
import java.util.concurrent.ThreadPoolExecutor;
4042
import java.util.concurrent.TimeUnit;
@@ -820,6 +822,39 @@ public void testRejectedExecutionError() {
820822
}
821823
}
822824

825+
@Test
826+
public void testThreadStarvationBeforeWorkflowThreadStarts() throws InterruptedException {
827+
ExecutorService executor = Executors.newSingleThreadExecutor();
828+
CountDownLatch executorThreadOccupied = new CountDownLatch(1);
829+
CountDownLatch releaseExecutorThread = new CountDownLatch(1);
830+
executor.submit(
831+
() -> {
832+
executorThreadOccupied.countDown();
833+
releaseExecutorThread.await();
834+
return null;
835+
});
836+
executorThreadOccupied.await();
837+
838+
DeterministicRunner runner =
839+
new DeterministicRunnerImpl(
840+
executor::submit,
841+
DummySyncWorkflowContext.newDummySyncWorkflowContext(),
842+
() -> fail("workflow code should not start while the executor thread is occupied"));
843+
844+
try {
845+
// Try starting the root workflow thread, but it won't run because the executor is tied up.
846+
// We should detect this and throw a PotentialDeadlockException.
847+
PotentialDeadlockException e =
848+
Assert.assertThrows(
849+
PotentialDeadlockException.class, () -> runner.runUntilAllBlocked(10));
850+
assertTrue(e.getMessage().contains("could not start executing within 10ms"));
851+
} finally {
852+
executor.shutdownNow();
853+
releaseExecutorThread.countDown();
854+
assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
855+
}
856+
}
857+
823858
@Test
824859
public void testCloseBlockedUntilDone() throws InterruptedException {
825860
final int THREAD_SLEEP_MS = 2000;

0 commit comments

Comments
 (0)