private void run() {
        try {
            while (!Thread.currentThread().isInterrupted() && !isStopping.get()) {
                try {
                    // If we have been paused, then block until we are resumed.
                    while (!pauseSemaphore.tryAcquire(5, TimeUnit.MINUTES)) {
                        // Drop out periodically and debug in case we need to know why things are stuck
                        LOGGER.debug(() -> LogUtil.message(
                                "Parallel executor '{}' waiting to be " +
                                "resumed, availablePermits: {}, isPaused: {}, isStopped: {}",
                                threadNamePrefix, pauseSemaphore.availablePermits(), isPaused, isStopped));
                    }
                    if (isStopping.get()) {
                        // Stop any tasks being kicked off when we are shutting down
                        break;
                    }
                    // Got our permit, so run the task
                    runTask();
                } catch (InterruptedException e) {
                    // Don't reset the interrupted flag as the thread is going straight back to the pool
                    throw new UncheckedInterruptedException(e);
                } finally {
                    // Release the one permit our thread acquired
                    LOGGER.debug(() -> LogUtil.message("About to release. availablePermits: {}, isPaused: {}",
                            pauseSemaphore.availablePermits(), isPaused));
                    // If we are paused we don't want to release the permit as that would allow another
                    // task to run with that permit.

                    // There is a risk here that isPaused is set to true AFTER we check it, and thus we release
                    // when we shouldn't. All that means is the semaphore has more permits than threads. This is
                    // fine as a subsequent pause will drain all. The converse case of not releasing when we should
                    // have is more troublesome as we end up with fewer permits than threads. Hence, the sync block
                    // for that case, plus a lock while paused is not a performance hit.
                    if (isPaused.get()) {
                        synchronized (this) {
                            if (!isPaused.get()) {
                                pauseSemaphore.release();
                            }
                        }
                    } else {
                        pauseSemaphore.release();
                    }
                }
            }
        } catch (final UncheckedInterruptedException e) {
            LOGGER.debug(e::getMessage, e);
        } catch (final RuntimeException e) {
            LOGGER.error(e::getMessage, e);
        }
    }
If true, pause the executor's threads after they have completed their current task.
if false, resume all the executor's threads.
If already in the desired state, is a noop.
stroom-proxy/stroom-proxy-repo/src/main/java/stroom/proxy/repo/ParallelExecutor.java