
com.couchbase.lite.internal.AbstractExecutionService Maven / Gradle / Ivy
//
// Copyright (c) 2020, 2017 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.couchbase.lite.internal;
import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import com.couchbase.lite.LogDomain;
import com.couchbase.lite.internal.support.Log;
import com.couchbase.lite.internal.utils.Preconditions;
/**
* Base ExecutionService that provides the default implementation of serial and concurrent
* executor.
*/
public abstract class AbstractExecutionService implements ExecutionService {
//---------------------------------------------
// Constants
//---------------------------------------------
private static final LogDomain DOMAIN = LogDomain.DATABASE;
private static final int DUMP_INTERVAL_MS = 2000; // 2 seconds
@VisibleForTesting
public static final int MIN_CAPACITY = 64;
private static final Object DUMP_LOCK = new Object();
//---------------------------------------------
// Class members
//---------------------------------------------
private static long lastDump;
//---------------------------------------------
// Types
//---------------------------------------------
@VisibleForTesting
static class InstrumentedTask implements Runnable {
// Putting a `new Exception()` here is useful but extremely expensive
@SuppressWarnings("PMD.FinalFieldCouldBeStatic")
final Exception origin = null;
@NonNull
private final Runnable task;
private final long createdAt = System.currentTimeMillis();
private long startedAt;
private long finishedAt;
private long completedAt;
@Nullable
private volatile Runnable onComplete;
InstrumentedTask(@NonNull Runnable task) { this(task, null); }
InstrumentedTask(@NonNull Runnable task, @Nullable Runnable onComplete) {
this.task = task;
this.onComplete = onComplete;
}
public void setCompletion(@NonNull Runnable onComplete) { this.onComplete = onComplete; }
@SuppressWarnings("PMD.AvoidCatchingThrowable")
public void run() {
try {
startedAt = System.currentTimeMillis();
task.run();
finishedAt = System.currentTimeMillis();
}
catch (Throwable t) {
Log.w(
LogDomain.DATABASE,
"Uncaught exception on thread " + Thread.currentThread().getName() + " in " + this,
t);
throw t;
}
finally {
final Runnable completionTask = onComplete;
if (completionTask != null) { completionTask.run(); }
}
completedAt = System.currentTimeMillis();
}
@NonNull
@Override
public String toString() {
return "task[" + createdAt + "," + startedAt + "," + finishedAt + "," + completedAt + " @" + task + "]";
}
}
/**
* This executor schedules tasks on an underlying thread pool executor
* (probably some application-wide executor: the Async Task's on Android).
*
* If the underlying executor is low on resources, this executor reverts
* to serial execution, using an unbounded pending queue.
*
* If the executor is stopped while there are unscheduled pending tasks
* (in the pendingTask queue), all of those tasks are simply discarded.
* If the pendingTask queue is non-empty, either the head task is scheduled
* or needsRestart
is true (see below) .
*
* Soft resource exhaustion, spaceAvailable
is intended to make it
* unlikely that this executor ever encounters a RejectedExecutionException
.
* There are two circumstances under which a RejectedExecutionException
* is possible:
*
* The underlying executor rejects the execution of a new task, even though
* spaceAvailable
returns true. This exception will be passed back
* to client code.
* A task on the pending queue attempts to schedule the next task from the queue
* for execution. When this happens, the queue is stalled and needsRestart
* is set true. Subsequent calls to execute
will make a best-effort attempt
* to restart the queue.
*
*/
private static class ConcurrentExecutor implements CloseableExecutor {
@NonNull
private final ThreadPoolExecutor executor;
@GuardedBy("this")
@NonNull
private final Queue pendingTasks = new LinkedList<>();
// a non-null stop latch is the flag that this executor has been stopped
@GuardedBy("this")
@Nullable
private CountDownLatch stopLatch;
@GuardedBy("this")
private int running;
@GuardedBy("this")
private boolean needsRestart;
ConcurrentExecutor(@NonNull ThreadPoolExecutor executor) {
Preconditions.assertNotNull(executor, "executor");
this.executor = executor;
}
/**
* Schedule a task for concurrent execution.
* There are absolutely no guarantees about execution order, on this executor,
* particularly once it fails back to using the pending task queue.
* If there is insufficient room to schedule the task, safely, on the underlying
* executor, the task is added to the pendingTask queue and executed when space
* is available.
* This method may throw a RejectedExecutionException
if the underlying
* executor's resources are completely exhausted even though spaceAvailable
* returns true.
*
* @param task a task for concurrent execution.
* @throws ExecutorClosedException if the executor has been stopped
* @throws RejectedExecutionException if the underlying executor rejects the task
*/
@Override
public void execute(@NonNull Runnable task) {
Preconditions.assertNotNull(task, "task");
final int pendingTaskCount;
synchronized (this) {
if (stopLatch != null) { throw new ExecutorClosedException("Executor has been stopped"); }
if (spaceAvailable()) {
if (needsRestart) { restartQueue(); }
executeTask(new InstrumentedTask(task, this::finishTask));
return;
}
pendingTasks.add(new InstrumentedTask(task));
pendingTaskCount = pendingTasks.size();
if (needsRestart || (pendingTaskCount == 1)) { restartQueue(); }
}
Log.w(DOMAIN, "Parallel executor overflow: " + pendingTaskCount);
}
/**
* Stop the executor.
* If there are pending (unscheduled) tasks, they are abandoned.
* If this call returns false, the executor has *not* yet stopped: tasks it scheduled are still running.
*
* @param timeout time to wait for shutdown
* @param unit time unit for shutdown wait
* @return true if all currently scheduled tasks have completed
*/
@Override
public boolean stop(long timeout, @NonNull TimeUnit unit) {
Preconditions.assertThat(timeout, "timeout must be >= 0", x -> x >= 0);
Preconditions.assertNotNull(unit, "time unit");
final CountDownLatch latch;
synchronized (this) {
if (stopLatch == null) {
pendingTasks.clear();
stopLatch = new CountDownLatch(1);
}
if (running <= 0) { return true; }
latch = stopLatch;
}
try { return latch.await(timeout, unit); }
catch (InterruptedException ignore) { }
return false;
}
void finishTask() {
final CountDownLatch latch;
synchronized (this) {
if (--running > 0) { return; }
latch = stopLatch;
}
if (latch != null) { latch.countDown(); }
}
// Called on completion of the task at the head of the pending queue.
void scheduleNext() {
synchronized (this) {
// the executor has been stopped
if (pendingTasks.size() <= 0) { return; }
// completing task is head of queue: remove it
pendingTasks.remove();
// run as many tasks as possible
try {
while (true) {
final InstrumentedTask task = pendingTasks.peek();
if (task == null) { return; }
if (!spaceAvailable()) { break; }
task.setCompletion(this::finishTask);
executeTask(task);
pendingTasks.remove();
}
}
catch (RejectedExecutionException ignore) { }
// assert: on exiting the loop, head of queue is first unexecutable (soft or hard) task
// it has not been submitted, successfully, for execution.
restartQueue();
}
}
// assert: queue is not empty.
private void restartQueue() {
final InstrumentedTask task = pendingTasks.peek();
try {
if (task != null) {
task.setCompletion(this::scheduleNext);
executeTask(task);
}
needsRestart = false;
return;
}
catch (RejectedExecutionException ignore) { }
needsRestart = true;
}
@GuardedBy("this")
private void executeTask(@NonNull InstrumentedTask newTask) {
try {
executor.execute(newTask);
running++;
}
catch (RejectedExecutionException e) {
dumpExecutorState(newTask, e);
throw e;
}
}
// Note that this is only accurate at the moment it is called...
private boolean spaceAvailable() { return executor.getQueue().remainingCapacity() > MIN_CAPACITY; }
// This shouldn't happen. Checking `spaceAvailable` should guarantee that the
// underlying executor always has resources when we attempt to execute something.
private void dumpExecutorState(@Nullable InstrumentedTask current, @Nullable RejectedExecutionException ex) {
if (throttled()) { return; }
dumpServiceState(executor, "size: " + running, ex);
Log.w(DOMAIN, "==== Concurrent Executor status: " + this);
if (needsRestart) { Log.w(DOMAIN, "= stalled"); }
if (current != null) { Log.w(DOMAIN, "== Current task: " + current, current.origin); }
final ArrayList waiting = new ArrayList<>(pendingTasks);
Log.w(DOMAIN, "== Pending tasks: " + waiting.size());
int n = 0;
for (InstrumentedTask t: waiting) { Log.w(DOMAIN, "@" + (++n) + ": " + t, t.origin); }
}
}
/**
* Serial execution, patterned after AsyncTask's executor.
* Tasks are queued on an unbounded queue and executed one at a time
* on an underlying executor: the head of the queue is the currently running task.
* Since this executor can have at most two tasks scheduled on the underlying
* executor, ensuring space on that executor makes it unlikely that
* a serial executor will refuse a task for execution.
*/
private static class SerialExecutor implements CloseableExecutor {
@NonNull
private final ThreadPoolExecutor executor;
@GuardedBy("this")
@NonNull
private final Queue pendingTasks = new LinkedList<>();
// a non-null stop latch is the flag that this executor has been stopped
@GuardedBy("this")
@Nullable
private CountDownLatch stopLatch;
@GuardedBy("this")
private boolean needsRestart;
SerialExecutor(@NonNull ThreadPoolExecutor executor) {
Preconditions.assertNotNull(executor, "executor");
this.executor = executor;
}
/**
* Schedule a task for in-order execution.
*
* @param task a task to be executed after all currently pending tasks.
* @throws ExecutorClosedException if the executor has been stopped
* @throws RejectedExecutionException if the underlying executor rejects the task
*/
@Override
public void execute(@NonNull Runnable task) {
Preconditions.assertNotNull(task, "task");
synchronized (this) {
if (stopLatch != null) { throw new ExecutorClosedException("Executor has been stopped"); }
pendingTasks.add(new InstrumentedTask(task, this::scheduleNext));
if (needsRestart || (pendingTasks.size() == 1)) { executeTask(null); }
}
}
/**
* Stop the executor.
* If this call returns false, the executor has *not* yet stopped.
* It will continue to run tasks from its queue until all have completed.
*
* @param timeout time to wait for shutdown
* @param unit time unit for shutdown wait
* @return true if all currently scheduled tasks completed before the shutdown
*/
@Override
public boolean stop(long timeout, @NonNull TimeUnit unit) {
Preconditions.assertThat(timeout, "timeout must be >= 0", x -> x >= 0);
Preconditions.assertNotNull(unit, "time unit");
final CountDownLatch latch;
synchronized (this) {
if (stopLatch == null) { stopLatch = new CountDownLatch(1); }
if (pendingTasks.size() <= 0) { return true; }
latch = stopLatch;
}
try { return latch.await(timeout, unit); }
catch (InterruptedException ignore) { }
return false;
}
// Called on completion of the task at the head of the pending queue.
private void scheduleNext() {
final CountDownLatch latch;
synchronized (this) {
executeTask(pendingTasks.remove());
latch = (pendingTasks.size() > 0) ? null : stopLatch;
}
if (latch != null) { latch.countDown(); }
}
@GuardedBy("this")
private void executeTask(@Nullable InstrumentedTask prevTask) {
final InstrumentedTask nextTask = pendingTasks.peek();
if (nextTask == null) { return; }
try {
executor.execute(nextTask);
needsRestart = false;
}
catch (RejectedExecutionException e) {
needsRestart = true;
dumpExecutorState(e, prevTask);
}
}
private void dumpExecutorState(@NonNull RejectedExecutionException ex, @Nullable InstrumentedTask prev) {
if (throttled()) { return; }
dumpServiceState(executor, "size: " + pendingTasks.size(), ex);
Log.w(DOMAIN, "==== Serial Executor status: " + this);
if (needsRestart) { Log.w(DOMAIN, "= stalled"); }
if (prev != null) { Log.w(DOMAIN, "== Previous task: " + prev, prev.origin); }
if (pendingTasks.isEmpty()) { Log.w(DOMAIN, "== Queue is empty"); }
else {
final ArrayList waiting = new ArrayList<>(pendingTasks);
final InstrumentedTask current = waiting.remove(0);
Log.w(DOMAIN, "== Current task: " + current, current.origin);
Log.w(DOMAIN, "== Pending tasks: " + waiting.size());
int n = 0;
for (InstrumentedTask t: waiting) { Log.w(DOMAIN, "@" + (++n) + ": " + t, t.origin); }
}
}
}
//---------------------------------------------
// Class methods
//---------------------------------------------
// check `throttled()` before calling.
static void dumpServiceState(@NonNull Executor ex, @NonNull String msg, @Nullable Exception e) {
Log.w(LogDomain.DATABASE, "====== Catastrophic failure of executor " + ex + ": " + msg, e);
final Map stackTraces = Thread.getAllStackTraces();
Log.w(DOMAIN, "==== Threads: " + stackTraces.size());
for (Map.Entry stack: stackTraces.entrySet()) {
Log.w(DOMAIN, "== Thread: " + stack.getKey());
for (StackTraceElement frame: stack.getValue()) { Log.w(DOMAIN, " at " + frame); }
}
if (!(ex instanceof ThreadPoolExecutor)) { return; }
final ArrayList waiting = new ArrayList<>(((ThreadPoolExecutor) ex).getQueue());
Log.w(DOMAIN, "==== Executor queue: " + waiting.size());
int n = 0;
for (Runnable r: waiting) {
final Exception orig = (!(r instanceof InstrumentedTask)) ? null : ((InstrumentedTask) r).origin;
Log.w(DOMAIN, "@" + (n++) + ": " + r, orig);
}
}
static boolean throttled() {
final long now = System.currentTimeMillis();
synchronized (DUMP_LOCK) {
if ((now - lastDump) < DUMP_INTERVAL_MS) { return true; }
lastDump = now;
}
return false;
}
//---------------------------------------------
// Instance members
//---------------------------------------------
@NonNull
private final ThreadPoolExecutor baseExecutor;
@NonNull
private final ConcurrentExecutor concurrentExecutor;
//---------------------------------------------
// Constructor
//---------------------------------------------
protected AbstractExecutionService(@NonNull ThreadPoolExecutor baseExecutor) {
this.baseExecutor = baseExecutor;
concurrentExecutor = new ConcurrentExecutor(baseExecutor);
}
//---------------------------------------------
// Public methods
//---------------------------------------------
@NonNull
@Override
public CloseableExecutor getSerialExecutor() { return new SerialExecutor(baseExecutor); }
@NonNull
@Override
public CloseableExecutor getConcurrentExecutor() { return concurrentExecutor; }
//---------------------------------------------
// Package-private methods
//---------------------------------------------
@VisibleForTesting
void dumpExecutorState() { concurrentExecutor.dumpExecutorState(null, new RejectedExecutionException()); }
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy