Merge changes I9e9b129f,Id98ef781 * changes: WorkQueue: Prevent premature removal of tasks on cancellation WorkQueue: Skip buggy isEmpty() check
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java index d8d2c9b..0e0e9c0 100644 --- a/java/com/google/gerrit/server/git/WorkQueue.java +++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -648,10 +648,7 @@ } void remove(Task<?> task) { - boolean isRemoved = all.remove(task.getTaskId(), task); - if (isRemoved && !listeners.isEmpty()) { - cancelIfParked(task); - } + all.remove(task.getTaskId(), task); } void cancelIfParked(Task<?> task) { @@ -880,6 +877,7 @@ @CanIgnoreReturnValue public boolean cancel(boolean mayInterruptIfRunning) { if (task.cancel(mayInterruptIfRunning)) { + boolean isSetRunningDuringCancellation = false; // Tiny abuse of runningState: if the task needs to know it // was canceled (to clean up resources) and it hasn't started // yet the task's run method won't execute. So we tag it @@ -888,6 +886,7 @@ // if (runnable instanceof CancelableRunnable) { if (runningState.compareAndSet(null, State.RUNNING)) { + isSetRunningDuringCancellation = true; ((CancelableRunnable) runnable).cancel(); } else if (runnable instanceof CanceledWhileRunning) { ((CanceledWhileRunning) runnable).setCanceledWhileRunning(); @@ -902,8 +901,11 @@ ((Future<?>) runnable).cancel(mayInterruptIfRunning); } - executor.remove(this); - executor.purge(); + if (isSetRunningDuringCancellation || runningState.get() == null) { + executor.remove(this); + executor.purge(); + } + executor.cancelIfParked(this); return true; } return false;
diff --git a/javatests/com/google/gerrit/acceptance/server/util/WorkQueueIT.java b/javatests/com/google/gerrit/acceptance/server/util/WorkQueueIT.java index 21a4d96..6c86b68 100644 --- a/javatests/com/google/gerrit/acceptance/server/util/WorkQueueIT.java +++ b/javatests/com/google/gerrit/acceptance/server/util/WorkQueueIT.java
@@ -18,11 +18,16 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.extensions.annotations.Exports; +import com.google.gerrit.server.config.ConfigResource; import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.git.WorkQueue.Task.State; +import com.google.gerrit.server.restapi.config.ListTasks; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Module; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -45,12 +50,14 @@ } private static final Integer FIXED_RATE_SCHEDULE_INITIAL_DELAY = 0; - private static final Integer FIXED_RATE_SCHEDULE_INTERVAL_MILLI_SEC = 1000; + private static final Integer FIXED_RATE_SCHEDULE_INTERVAL_MILLI_SEC = 200; private static final Integer POOL_CORE_SIZE = 8; private static final String QUEUE_NAME = "test-Queue"; private static final Integer EXCEPT_RUN_TIMES = 2; + private static final Integer TIMEOUT_MILLIS = 500; private final CountDownLatch downLatch = new CountDownLatch(EXCEPT_RUN_TIMES); @Inject private WorkQueue workQueue; + @Inject private ListTasks listTasks; private TestListener testListener; @Override @@ -82,4 +89,50 @@ assertThat(ifRunMoreThanOnce).isTrue(); testExecutor.shutdownNow(); } + + @Test + public void testCanceledTaskStaysUntilFinished() throws Exception { + ScheduledExecutorService testExecutor = workQueue.createQueue(POOL_CORE_SIZE, QUEUE_NAME); + CountDownLatch latch = new CountDownLatch(1); + Future taskFuture = + testExecutor.submit( + () -> { + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + assertTasksInStateEventually(QUEUE_NAME, State.RUNNING, 1); + + taskFuture.cancel(false); + assertTasksInStateEventually(QUEUE_NAME, State.CANCELLED, 1); + + latch.countDown(); + // task is now removed after completion + assertEventually( + () -> + listTasks.apply(new ConfigResource()).value().stream() + .noneMatch(t -> t.queueName.equals(QUEUE_NAME))); + testExecutor.shutdownNow(); + } + + public void assertTasksInStateEventually(String queue, State expectedState, int expectedCount) + throws Exception { + assertEventually( + () -> + expectedCount + == listTasks.apply(new ConfigResource()).value().stream() + .filter(t -> t.queueName.equals(queue)) + .filter(t -> t.state.equals(expectedState)) + .count()); + } + + public void assertEventually(Callable<Boolean> r) throws Exception { + long ms = 0; + while (r.call() != true) { + assertThat(ms++).isLessThan(TIMEOUT_MILLIS); + TimeUnit.MILLISECONDS.sleep(1); + } + } }