/* * Copyright (c) 2014 the original author or authors * * 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 io.werval.runtime; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import io.werval.api.ApplicationExecutors; import io.werval.api.Config; import io.werval.api.Global; import io.werval.runtime.context.ContextExecutor; import io.werval.runtime.util.ForkJoinPoolNamedThreadFactory; import io.werval.runtime.util.NamedThreadFactory; import io.werval.spi.ApplicationSPI; import io.werval.util.Couple; import io.werval.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.Collections.emptyMap; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static io.werval.runtime.ConfigKeys.APP_EXECUTORS; import static io.werval.runtime.ConfigKeys.APP_EXECUTORS_DEFAULT; import static io.werval.runtime.ConfigKeys.APP_EXECUTORS_SHUTDOWN_TIMEOUT; import static io.werval.util.IllegalArguments.ensureNotEmpty; /** * Application Executors. */ /* package */ class ApplicationExecutorsInstance implements ApplicationExecutors { private static final Logger LOG = LoggerFactory.getLogger( ApplicationExecutorsInstance.class ); private static final int DEFAULT_POOL_SIZE = Runtime.getRuntime().availableProcessors(); /** * A Fixed ThreadPool that handle uncaught exceptions. * * See http://stackoverflow.com/questions/1838923/why-is-uncaughtexceptionhandler-not-called-by-executorservice */ private static class UncaughtExceptionHandlerThreadPool extends ThreadPoolExecutor { private final ApplicationSPI application; public UncaughtExceptionHandlerThreadPool( ApplicationSPI application, int size, ThreadFactory threadFactory ) { super( size, size, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), threadFactory ); this.application = application; } @Override protected void afterExecute( Runnable runnable, Throwable cause ) { if( cause != null ) { handleUncaughtException( application, application.global(), cause ); } } } private static final class UncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private final ApplicationSPI application; private UncaughtExceptionHandler( ApplicationSPI application ) { this.application = application; } @Override public void uncaughtException( Thread thread, Throwable cause ) { handleUncaughtException( application, application.global(), cause ); } } private static void handleUncaughtException( ApplicationSPI application, Global global, Throwable cause ) { // Clean-up stacktrace Throwable rootCause = ErrorHandling.cleanUpStackTrace( application, LOG, cause ); ApplicationExecutors executors = application.executors(); try { // Notify Global if( executors.inDefaultExecutor() ) { global.onApplicationError( application, rootCause ); } else { executors.runAsync( () -> global.onApplicationError( application, rootCause ) ).join(); } } catch( Exception ex ) { // Add as suppressed and replay Global default behaviour. This serve as a fault barrier rootCause.addSuppressed( ex ); if( executors.inDefaultExecutor() ) { new Global().onApplicationError( application, rootCause ); } else { executors.runAsync( () -> new Global().onApplicationError( application, rootCause ) ).join(); } } // Record error application.errors().record( null, "Uncaught Exception: " + rootCause.getClass() + ": " + rootCause.getMessage(), rootCause ); } private final ApplicationSPI application; private String defaultExecutor; private String defaultExecutorThreadNamePrefix; private Long shutdownTimeoutMillis; private Map<String, ExecutorService> executors = emptyMap(); private Map<String, String> executorsThreadNamePrefixes = emptyMap(); private String summary; /* package */ ApplicationExecutorsInstance( ApplicationSPI application ) { this.application = application; } /* package */ void activate() { this.defaultExecutor = application.config().string( APP_EXECUTORS_DEFAULT ); this.defaultExecutorThreadNamePrefix = application.config().string( APP_EXECUTORS + "." + defaultExecutor + ".thread_name_prefix" ); this.shutdownTimeoutMillis = application.config().milliseconds( APP_EXECUTORS_SHUTDOWN_TIMEOUT ); Config executorsConfig = application.config().atPath( APP_EXECUTORS ); int executorsCount = executorsConfig.subKeys().size(); this.executors = new HashMap<>( executorsCount - 1 ); this.executorsThreadNamePrefixes = new HashMap<>( executorsCount ); Map<String, Couple<String, Integer>> summaryData = new HashMap<>( executorsCount ); for( String executorName : executorsConfig.subKeys() ) { // Skip the default executor and shutdown config entries if( "default".equals( executorName ) || "shutdown".equals( executorName ) ) { continue; } // Load configuration String typeKey = executorName + ".type"; String countKey = executorName + ".count"; String namePrefixKey = executorName + ".thread_name_prefix"; String type = executorsConfig.stringOptional( typeKey ).orElse( "thread-pool" ); int count = executorsConfig.intOptional( countKey ).orElse( DEFAULT_POOL_SIZE ); String namePrefix = executorsConfig.stringOptional( namePrefixKey ).orElse( executorName + "_thread" ); // Override default executor in development mode // if( defaultExecutor.equals( executorName ) && application.mode() == Mode.DEV ) // { // // TODO Investigate how we could limit concurrency in development mode! // type = "thread-pool"; // count = 1; // } // Create executor ExecutorService executor; switch( type ) { case "fork-join": executor = new ContextExecutor( new ForkJoinPool( count, new ForkJoinPoolNamedThreadFactory( namePrefix ), new UncaughtExceptionHandler( application ), false ) ); executors.put( executorName, executor ); break; case "thread-pool": default: executor = new ContextExecutor( new UncaughtExceptionHandlerThreadPool( application, count, new NamedThreadFactory( namePrefix ) ) ); executors.put( executorName, executor ); } executorsThreadNamePrefixes.put( executorName, namePrefix ); summaryData.put( executorName, Couple.of( type, count ) ); } // Generate summary StringBuilder summaryBuilder = new StringBuilder(); Couple<String, Integer> defaultData = summaryData.get( defaultExecutor ); summaryBuilder.append( defaultExecutor ).append( " (default): " ) .append( defaultData.left() ).append( "[" ).append( defaultData.right() ).append( "]\n" ); for( Map.Entry<String, Couple<String, Integer>> entry : summaryData.entrySet() ) { String name = entry.getKey(); if( !defaultExecutor.equals( name ) ) { Couple<String, Integer> data = entry.getValue(); summaryBuilder.append( name ).append( ": " ) .append( data.left() ).append( "[" ).append( data.right() ).append( "]\n" ); } } summary = summaryBuilder.toString(); } /* package */ void passivate() { // Default executor first defaultExecutor().shutdown(); // All other executors for( Map.Entry<String, ExecutorService> executor : executors.entrySet() ) { // Skip default if( defaultExecutor.equals( executor.getKey() ) ) { continue; } executor.getValue().shutdown(); } // Await for default executor Map<String, List<Runnable>> notRun = new LinkedHashMap<>( executors.size() ); try { defaultExecutor().awaitTermination( shutdownTimeoutMillis, MILLISECONDS ); } catch( InterruptedException ex ) { List<Runnable> failed = defaultExecutor().shutdownNow(); if( !failed.isEmpty() ) { notRun.put( defaultExecutor, failed ); } } // Shutdown all other executors now for( Map.Entry<String, ExecutorService> executor : executors.entrySet() ) { // Skip default if( defaultExecutor.equals( executor.getKey() ) ) { continue; } List<Runnable> failed = executor.getValue().shutdownNow(); if( !failed.isEmpty() ) { notRun.put( defaultExecutor, failed ); } } // Eventually log the tasks that were awaiting execution if( !notRun.isEmpty() ) { LOG.warn( "Some Application Executors failed to complete tasks before shutdown timeout: {} - {}", notRun, "Watch out! Zombie threads!" ); } // Cleanup defaultExecutor = null; shutdownTimeoutMillis = null; executors = emptyMap(); executorsThreadNamePrefixes = emptyMap(); summary = null; } @Override public ExecutorService defaultExecutor() { return executors.get( defaultExecutor ); } @Override public boolean inDefaultExecutor() { return Thread.currentThread().getName().startsWith( defaultExecutorThreadNamePrefix ); } @Override public ExecutorService executor( String executorName ) { ensureNotEmpty( "Application Executor Name", executorName ); return executors.get( executorName ); } @Override public boolean inExecutor( String executorName ) { ensureNotEmpty( "Application Executor Name", executorName ); String executorThreadNamePrefix = executorsThreadNamePrefixes.get( executorName ); return Strings.hasText( executorThreadNamePrefix ) && Thread.currentThread().getName().startsWith( executorThreadNamePrefix ); } @Override public String toString() { return summary; } }