/* * 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. * * Contributions from 2013-2017 where performed either by US government * employees, or under US Veterans Health Administration contracts. * * US Veterans Health Administration contributions by government employees * are work of the U.S. Government and are not subject to copyright * protection in the United States. Portions contributed by government * employees are USGovWork (17USC ยง105). Not subject to copyright. * * Contribution by contractors to the US Veterans Health Administration * during this period are contractually contributed under the * Apache License, Version 2.0. * * See: https://www.usa.gov/government-works * * Contributions prior to 2013: * * Copyright (C) International Health Terminology Standards Development Organisation. * Licensed under the Apache License, Version 2.0. * */ package sh.isaac.api.util; //~--- JDK imports ------------------------------------------------------------ import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; //~--- non-JDK imports -------------------------------------------------------- import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.glassfish.hk2.runlevel.RunLevel; import org.jvnet.hk2.annotations.Service; import sh.isaac.api.Get; import sh.isaac.api.LookupService; //~--- classes ---------------------------------------------------------------- /** * {@link WorkExecutors} * * Generally available thread pools for doing background processing in an ISAAC application. * * The {@link #getForkJoinPoolExecutor()} that this provides is identical to the @{link {@link ForkJoinPool#commonPool()} * with the exception that it will bottom out at 6 processing threads, rather than 1, to help prevent * deadlock situations in common ISAAC usage patterns. This has an unbounded queue depth, and LIFO behavior. * * The {@link #getPotentiallyBlockingExecutor()} that this provides is a standard thread pool with (up to) the same number of threads * as there are cores present on the computer - with a minimum of 6 threads. This executor has no queue - internally * it uses a {@link SynchronousQueue} - so if no thread is available to accept the task being queued, it will block * submission of the task until a thread is available to accept the job. * * The {@link #getExecutor()} that this provides is a standard thread pool with (up to) the same number of threads * as there are cores present on the computer - with a minimum of 6 threads. This executor has an unbounded queue * depth, and FIFO behavior. * * The {@link #getIOExecutor()} that this provides is a standard thread pool with 4 threads. This executor has an unbounded queue * depth, and FIFO behavior. This executor is good for jobs that tend to block on disk IO, where you don't want many running in parallel. * * If you wish to use this code outside of an HK2 managed application (or in utility code that may operate in and out * of an HK2 environment), please use the {@link #get()} method to get a handle to this class * * @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a> */ @Service @RunLevel(value = -2) public class WorkExecutors { /** The Constant log. */ private static final Logger log = LogManager.getLogger(); /** The non HK 2 instance. */ private volatile static WorkExecutors nonHK2Instance = null; //~--- fields -------------------------------------------------------------- /** The fork join executor. */ private ForkJoinPool forkJoinExecutor; /** The blocking thread pool executor. */ private ThreadPoolExecutor blockingThreadPoolExecutor; /** The thread pool executor. */ private ThreadPoolExecutor threadPoolExecutor; /** The io thread pool executor. */ private ThreadPoolExecutor ioThreadPoolExecutor; /** The scheduled executor. */ private ScheduledExecutorService scheduledExecutor; //~--- constructors -------------------------------------------------------- /** * Instantiates a new work executors. */ private WorkExecutors() { // For HK2 only } //~--- methods ------------------------------------------------------------- /** * The main method. * * @param args the arguments * @throws InterruptedException the interrupted exception */ public static void main(String[] args) throws InterruptedException { final WorkExecutors we = new WorkExecutors(); we.startMe(); final AtomicInteger counter = new AtomicInteger(); for (int i = 0; i < 24; i++) { System.out.println("submit " + i); we.getPotentiallyBlockingExecutor().submit(() -> { final int id = counter.getAndIncrement(); System.out.println(id + " started"); try { Thread.sleep(5000); } catch (final InterruptedException e) { e.printStackTrace(); } System.out.println(id + " finished"); }); } Thread.sleep(7000); System.out.println("Blocking test over"); for (int i = 24; i < 48; i++) { System.out.println("submit " + i); we.getExecutor().submit(() -> { final int id = counter.getAndIncrement(); System.out.println(id + " started"); try { Thread.sleep(5000); } catch (final InterruptedException e) { e.printStackTrace(); } System.out.println(id + " finished"); }); } while (we.getExecutor() .getQueue() .size() > 0) { Thread.sleep(1000); } Thread.sleep(7000); } /** * Start me. */ @PostConstruct private void startMe() { log.info("Starting the WorkExecutors thread pools"); if (nonHK2Instance != null) { throw new RuntimeException( "Two instances of WorkExectors started! If HK2 will manage, startup HK2 properly first!"); } // The java default ForkJoinPool.commmonPool starts with only 1 thread, on 1 and 2 core systems, which can get us deadlocked pretty easily. final int procCount = Runtime.getRuntime() .availableProcessors(); final int parallelism = ((procCount - 1) < 6 ? 6 : procCount - 1); // set between 6 and 1 less than proc count (not less than 6) this.forkJoinExecutor = new ForkJoinPool(parallelism); final int corePoolSize = 2; final int maximumPoolSize = parallelism; final int keepAliveTime = 60; final TimeUnit timeUnit = TimeUnit.SECONDS; // The blocking executor this.blockingThreadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, new SynchronousQueue<>(), new NamedThreadFactory("ISAAC-B-work-thread", true)); this.blockingThreadPoolExecutor.setRejectedExecutionHandler((runnable, executor) -> { try { executor.getQueue() .offer(runnable, Long.MAX_VALUE, TimeUnit.HOURS); } catch (final Exception e) { throw new RejectedExecutionException("Interrupted while waiting to enqueue"); } }); // The non-blocking executor - set core threads equal to max - otherwise, it will never increase the thread count // with an unbounded queue. this.threadPoolExecutor = new ThreadPoolExecutor(maximumPoolSize, maximumPoolSize, keepAliveTime, timeUnit, new LinkedBlockingQueue<>(), new NamedThreadFactory("ISAAC-Q-work-thread", true)); this.threadPoolExecutor.allowCoreThreadTimeOut(true); // The IO non-blocking executor - set core threads equal to max - otherwise, it will never increase the thread count // with an unbounded queue. this.ioThreadPoolExecutor = new ThreadPoolExecutor(4, 4, keepAliveTime, timeUnit, new LinkedBlockingQueue<>(), new NamedThreadFactory("ISAAC-IO-work-thread", true)); this.ioThreadPoolExecutor.allowCoreThreadTimeOut(true); // Execute this once, early on, in a background thread - as randomUUID uses secure random - and the initial // init of secure random can block on many systems that don't have enough entropy occuring. The DB load process // should provide enough entropy to get it initialized, so it doesn't pause things later when someone requests a random UUID. getExecutor().execute(() -> UUID.randomUUID()); this.scheduledExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("ISAAC-Scheduled-Thread", true)); log.debug("WorkExecutors thread pools ready"); } /** * Stop me. */ @PreDestroy private void stopMe() { log.info("Stopping WorkExecutors thread pools"); if (this.forkJoinExecutor != null) { this.forkJoinExecutor.shutdownNow(); this.forkJoinExecutor = null; } if (this.blockingThreadPoolExecutor != null) { this.blockingThreadPoolExecutor.shutdownNow(); this.blockingThreadPoolExecutor = null; } if (this.threadPoolExecutor != null) { this.threadPoolExecutor.shutdownNow(); this.threadPoolExecutor = null; } if (this.ioThreadPoolExecutor != null) { this.ioThreadPoolExecutor.shutdownNow(); this.ioThreadPoolExecutor = null; } if (this.scheduledExecutor != null) { this.scheduledExecutor.shutdownNow(); this.scheduledExecutor = null; } nonHK2Instance = null; log.debug("Stopped WorkExecutors thread pools"); } //~--- get methods --------------------------------------------------------- /** * Gets the executor. * * @return The ISAAC common {@link ThreadPoolExecutor} - (behavior described in the class docs). * This is backed by an unbounded queue - it won't block / reject submissions because of being full. * This executor has processing threads linkes to the number of CPUs available. It is good for compute * intensive jobs. */ public ThreadPoolExecutor getExecutor() { return this.threadPoolExecutor; } /** * Gets the fork join pool executor. * * @return the ISAAC common {@link ForkJoinPool} instance - (behavior described in the class docs) * This is backed by an unbounded queue - it won't block / reject submissions because of being full. */ public ForkJoinPool getForkJoinPoolExecutor() { return this.forkJoinExecutor; } /** * If HK2 has been properly started, this method safely returns the instance setup by HK2. * If HK2 has NOT been started, this method will create a static instance, and return a reference to that. * A JVM shutdown listener is registered to handle the thread pool shutdown in this case. * It is illegal (and will throw a runtime error) to ask for the static instance of this before * starting HK2 and then start HK2 - if HK2 is in use in the system, that should manage the lifecycle. * * This method is the preferred mechanism to get a handle to the WorkExecutors class in an enviornment where * code may be executed both in and out of an HK2 managed instance. * * If your usage is only run inside an HK2 management environment, then you should prefer the HK2 standard mechanisms * such as: * {@link Get#workExecutors()} or {@link LookupService#getService(WorkExecutors.class)} (however the end result is the same) * * @return the work executors */ public static WorkExecutors get() { log.debug("In WorkExectors.get()"); if (LookupService.isInitialized() && (LookupService.getCurrentRunLevel() >= LookupService.WORKERS_STARTED_RUNLEVEL)) { log.debug("Handing back the HK2 managed instance"); return Get.workExecutors(); } else { log.debug("Returning static WorkExecutors instance"); if (nonHK2Instance == null) { synchronized (log) { if (nonHK2Instance == null) { log.debug("Setting up static WorkExecutors"); // if we aren't relying on the lookup service, we need to make sure the headless toolkit was installed (otherwise, the task APIs end up broken) LookupService.startupFxPlatform(); final WorkExecutors temp = new WorkExecutors(); temp.startMe(); nonHK2Instance = temp; Runtime.getRuntime() .addShutdownHook(new Thread(() -> { log.debug("Shutting down static instance of WorkExecutors"); nonHK2Instance.stopMe(); log.debug("stopped static instance of WorkExecutors"); })); } } } return nonHK2Instance; } } /** * Gets the IO executor. * * @return The ISAAC common IO {@link ThreadPoolExecutor} - (behavior described in the class docs). * This is backed by an unbounded queue - it won't block / reject submissions because of being full. * This executor differs from {@link #getExecutor()} by having a much smaller number of threads - good for * jobs that tend to block on IO. */ public ThreadPoolExecutor getIOExecutor() { return this.ioThreadPoolExecutor; } /** * Gets the potentially blocking executor. * * @return The ISAAC common {@link ThreadPoolExecutor} - (behavior described in the class docs). * This is a synchronous queue - if no thread is available to take a job, it will block until a thread * is available to accept the job. */ public ThreadPoolExecutor getPotentiallyBlockingExecutor() { return this.blockingThreadPoolExecutor; } /** * Gets the scheduled thread pool executor. * * @return the ISAAC common {@link ScheduledThreadPoolExecutor} instance - (behavior described in the class docs) * This pool only has a single thread - submitted jobs should be fast executing. */ public ScheduledExecutorService getScheduledThreadPoolExecutor() { return this.scheduledExecutor; } }