All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.apphosting.runtime.ThreadGroupPool Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.google.apphosting.runtime;

import com.google.auto.value.AutoBuilder;
import com.google.common.flogger.GoogleLogger;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * {@code ThreadGroupPool} is a very simple thread pool where each
 * pooled thread is in its own {@link ThreadGroup}.  Unfortunately
 * threads cannot be moved around between thread groups, so we just
 * pool (ThreadGroup, Thread) pairs.  If additional threads are
 * started in a thread group, they are expected to have exited before
 * the runnable provided to {@link #start} completes.  If this is not
 * the case, the thread will be dropped from the thread pool and
 * detailed diagnostics will be written to the log.
 *
 * 

Unlike thread names, thread group names are immutable so thread * groups will be named with a specified prefix with a counter * appended. The name of the main thread for each thread pool is * determined when {@link #start} is called. * */ public class ThreadGroupPool { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); public static Builder builder() { return new AutoBuilder_ThreadGroupPool_Builder(); } /** Builder for ThreadGroupPool. */ @AutoBuilder public abstract static class Builder { public abstract Builder setParentThreadGroup(ThreadGroup threadGroup); public abstract Builder setThreadGroupNamePrefix(String threadGroupNamePrefix); public abstract Builder setUncaughtExceptionHandler( UncaughtExceptionHandler uncaughtExceptionHandler); public abstract Builder setIgnoreDaemonThreads(boolean ignoreDaemonThreads); public abstract ThreadGroupPool build(); } private final ThreadGroup parentThreadGroup; private final String threadGroupNamePrefix; private final AtomicInteger threadGroupCounter; private final Queue waitingThreads; private final UncaughtExceptionHandler uncaughtExceptionHandler; private final boolean ignoreDaemonThreads; public ThreadGroupPool( ThreadGroup parentThreadGroup, String threadGroupNamePrefix, UncaughtExceptionHandler uncaughtExceptionHandler, boolean ignoreDaemonThreads) { this.parentThreadGroup = parentThreadGroup; this.threadGroupNamePrefix = threadGroupNamePrefix; this.threadGroupCounter = new AtomicInteger(0); this.waitingThreads = new ConcurrentLinkedQueue<>(); this.uncaughtExceptionHandler = uncaughtExceptionHandler; this.ignoreDaemonThreads = ignoreDaemonThreads; } /** * Execute {@code runnable} in a thread named {@code threadName}. * This may be a newly created thread or it may be a thread that was * was already used to run one or more previous invocations. * *

{@code runnable} can spawn other threads in the pooled * {@link ThreadGroup}, but they must all exit before the runnable * completes. Failure of the extra threads to complete will result * in a severe log message and the dropping of this thread from the * pool. * *

This method will block until the thread begins executing * {@code runnable}. If executing {@link Runnable#run} on * {@code runnable} throws an exception, the thread will not be * returned to the thread pool. */ public void start(String threadName, Runnable runnable) throws InterruptedException { PoolEntry entry = waitingThreads.poll(); if (entry == null) { entry = buildPoolEntry(); } initThread(entry.getMainThread(), threadName); entry.runInMainThread(runnable); } private void removeThread(PoolEntry entry) { waitingThreads.remove(entry); } private void returnThread(PoolEntry entry) { initThread(entry.getMainThread(), "Idle"); waitingThreads.add(entry); } public int waitingThreadCount() { return waitingThreads.size(); } private void initThread(Thread thread, String threadName) { thread.setName(threadName); thread.setUncaughtExceptionHandler(null); } private PoolEntry buildPoolEntry() { String name = threadGroupNamePrefix + threadGroupCounter.getAndIncrement(); ThreadGroup threadGroup = new ThreadGroup(parentThreadGroup, name) { @Override public void uncaughtException(Thread th, Throwable ex) { uncaughtExceptionHandler.uncaughtException(th, ex); } }; PoolEntry entry = new PoolEntry(threadGroup); entry.startMainThread(); return entry; } /** * If the current thread is main thread started in response to a * call to {@link #start}, this method will arrange for it to expect * to be "restarted." See {@link RestartableThread} for more * information. * * @throws IllegalStateException If the current thread is not a main * thread. */ public static CountDownLatch resetCurrentThread() throws InterruptedException { Thread thread = Thread.currentThread(); if (thread instanceof RestartableThread) { return ((RestartableThread) thread).reset(); } else { throw new IllegalStateException("Current thread is not a main request thread."); } } /** * {@code RestartableThread} is a thread that can be put to sleep * until {@link Thread#start} is called again. This is required for * background threads, which will be spawned normally and passed to * user code, but then need to block until user code invokes the * start method before proceeding. To facilitate this, calling code * needs to invoke {@link #reset} before returning the * thread to user code, and it can block on the returned latch to be * awoke when user code calls start. Note that subsequent start * calls will behave normally, including throwing an * {@link IllegalStateException} when appropriate. */ private static final class RestartableThread extends Thread { private final Object lock = new Object(); private CountDownLatch latch; RestartableThread(ThreadGroup threadGroup, Runnable runnable) { super(threadGroup, runnable); } public CountDownLatch reset() { synchronized (lock) { latch = new CountDownLatch(1); return latch; } } @SuppressWarnings("UnsynchronizedOverridesSynchronized") // synchronized on lock, then on super @Override public void start() { synchronized (lock) { if (latch != null) { latch.countDown(); latch = null; return; } } // No reset was pending, do the normal thing. super.start(); } @Override public Thread.State getState() { synchronized (lock) { if (latch != null) { // Thread has been reset, pretend it is not yet started. return Thread.State.NEW; } } return super.getState(); } } /** * {@code PoolEntry} is one entry in a {@link ThreadGroupPool} that * consists of a {@link ThreadGroup}, a single {@link Thread} within * that group, and a {@link SynchronousQueue} that is used to pass a * {@link Runnable} into the thread for execution. The entry itself * serves as a {@link Runnable} that forwards control the * {@link Runnable} received via the {@link Executor}, and then * verifies that no other threads remain in the {@link ThreadGroup} * before returning it to the pool. */ private class PoolEntry implements Runnable { final ThreadGroup threadGroup; /** * This SynchronousQueue is passed Runnables from the thread calling (via * {@link #start}) to one of the pooled threads waiting to execute * the Runnable. */ private final SynchronousQueue runnableQueue; private final RestartableThread mainThread; PoolEntry(ThreadGroup threadGroup) { this.threadGroup = threadGroup; this.runnableQueue = new SynchronousQueue<>(); mainThread = new RestartableThread(threadGroup, this); mainThread.setDaemon(false); } void startMainThread() { mainThread.start(); } Thread getMainThread() { return mainThread; } void runInMainThread(Runnable runnable) throws InterruptedException { if (!mainThread.isAlive()) { throw new IllegalStateException("Main thread is not running."); } runnableQueue.put(runnable); } @Override public void run() { try { while (true) { Runnable runnable; try { runnable = runnableQueue.take(); } catch (InterruptedException ex) { logger.atInfo().withCause(ex).log("Interrupted while waiting for next Runnable"); removeThread(this); return; } runnable.run(); if (otherThreadsLeftInThreadGroup()) { return; } if (Thread.interrupted()) { logger.atInfo().log("Not reusing %s, interrupt bit was set.", this); return; } returnThread(this); } } catch (Throwable th) { JavaRuntime.killCloneIfSeriousException(th); throw th; } } /** * Verifies that no other active threads are present in * {@code threadGroup}. If any threads are still running, log * their stack trace and return {@code true}. */ private boolean otherThreadsLeftInThreadGroup() { List threads = threadsInThreadGroup(); boolean otherThreads = false; if (threads.size() > 1) { for (Thread thread : threads) { if (thread != Thread.currentThread() && !(ignoreDaemonThreads && thread.isDaemon())) { // Throwable th = new Throwable(); // th.setStackTrace(thread.getStackTrace()); // logger.atSevere().withCause(th).log("Extra thread left running: %s", thread); otherThreads = true; } } } return otherThreads; } private List threadsInThreadGroup() { Thread[] threads = new Thread[50]; int threadCount; while ((threadCount = threadGroup.enumerate(threads, true)) == threads.length) { threads = new Thread[threads.length * 2]; } return Arrays.asList(threads).subList(0, threadCount); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy