WorkQueue: Fix parked-task core-pool size races When a task is parked, WorkQueue temporarily increments corePoolSize so a replacement worker can run. The parked slot must be decremented exactly once when that parked task is either unparked or cancelled. This change ensures that when a completing thread unparks or cleans up a ready or canceled task in updateParked(), it will not proceed to potentially start working on a queued task until the thread pool has been decremented, thus avoiding potentially temporarily running more tasks than allowed concurrently. Add TaskParkerIT coverage for interrupting a task while it is being unparked, plus a small helper to interrupt task threads by name. Change-Id: I3f9bdb42b34f1ec79763f93b23b54812fe77f7fe Release-Notes: Avoid running more WorkQueue tasks than allowed
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java index 95d6d5f..49d0be0 100644 --- a/java/com/google/gerrit/server/git/WorkQueue.java +++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -60,6 +60,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -347,6 +348,7 @@ private final CancellableCountDownLatch latch = new CancellableCountDownLatch(1); private final Task<?> task; private final Long priority = priorityGenerator.getAndIncrement(); + private final AtomicBoolean isParked = new AtomicBoolean(true); public ParkedTask(Task<?> task) { this.task = task; @@ -366,6 +368,7 @@ * method. */ public void cancel() { + close(); latch.cancel(); } @@ -383,12 +386,15 @@ } public void unpark() { + close(); latch.countDown(); } @Override public void close() { - incrementCorePoolSizeBy(-1); + if (isParked.compareAndSet(true, false)) { + incrementCorePoolSizeBy(-1); + } } }
diff --git a/javatests/com/google/gerrit/acceptance/server/util/TaskParkerIT.java b/javatests/com/google/gerrit/acceptance/server/util/TaskParkerIT.java index 3b82ebe..df63b69 100644 --- a/javatests/com/google/gerrit/acceptance/server/util/TaskParkerIT.java +++ b/javatests/com/google/gerrit/acceptance/server/util/TaskParkerIT.java
@@ -463,12 +463,34 @@ assertStateIsEventually(forwarder.task, State.PARKED); // interrupt the thread with parked task - for (Thread t : Thread.getAllStackTraces().keySet()) { - if (t.getName().contains(taskName)) { - t.interrupt(); - break; - } - } + interruptThreadContaining(taskName); + + assertCorePoolSizeIsEventually(1); + } + + @Test + public void interruptingTaskWhileUnparkingDoesNotDoubleDecrementCorePoolSize() + throws InterruptedException { + String taskName = "to-be-unparked"; + LatchedRunnable parkedRunnable = new LatchedRunnable(taskName); + LatchedRunnable blockerRunnable = new LatchedRunnable("blocker"); + assertCorePoolSizeIs(1); + + // park parkedRunnable + executor.execute(parkedRunnable); + parker.isReadyToStart.assertCalledEventuallyThenComplete(false); + assertCorePoolSizeIsEventually(2); + assertStateIsEventually(forwarder.task, State.PARKED); + + // start blockerRunnable and unblock it to trigger updateParked() from onStop() + executor.execute(blockerRunnable); + blockerRunnable.run.assertCalledEventuallyThenComplete(null); + + // Wait for updateParked() to poll parkedRunnable and call isReadyToStart(), then interrupt + // the parked thread before releasing isReadyToStart() as ready. + parker.isReadyToStart.assertCalledEventually(); + interruptThreadContaining(taskName); + parker.isReadyToStart.complete(true); assertCorePoolSizeIsEventually(1); } @@ -502,6 +524,15 @@ TaskListenerIT.assertTaskCountIsEventually(workQueue, count); } + private void interruptThreadContaining(String taskName) { + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().contains(taskName)) { + t.interrupt(); + break; + } + } + } + private void assertCorePoolSizeIs(int count) { assertThat(count).isEqualTo(((ScheduledThreadPoolExecutor) executor).getCorePoolSize()); }